一、乘方 \(\text{pow}\)
我们看数据范围:
对于 \(100\%\) 的数据,保证 \(1 \le a,b \le 10^9\)
可以轻易得知,即使没有别的限制,至少也应该用快速幂解决
而这题只有一个限制:如果答案大于 \(10^9\) 就输出 \(-1\)
用眼一看,这很简单啊,只要在快速幂计算答案时每轮判定一下答案是否大于 \(10^9\) 即可
或者不放心的话还可以判断 \(a\) 每一轮是否大于 \(10^9\)
但本题有坑:如果开始 \(a\) 就大于 \(10^9\),\(b\) 又是个偶数,那肯定是输出 \(-1\) 的
但由于 \(b\) 是偶数,第一轮时答案并没有乘 \(a\),第一轮结束后 \(a=a^2\) 造成溢出,从而导致答案错误
因此还要在开头判定一下 \(a,b\) 本身是否符合输出 \(-1\) 的条件
\(\text{Code:}\)
#include<bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
const int maxed=1e9;
inline int qpow(int u,int v)//快速幂板子
{
int res=1;
while(v)
{
if(v&1) res*=u;
v>>=1,u*=u;
if(res>maxed||res<=0) return -1;//判断答案是否溢出或是否大于 1e9
//这里其实应该也要判断一下 u 的范围
}
return res;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
freopen("pow.in","r",stdin);
freopen("pow.out","w",stdout);
//文件操作提交到洛谷的时候记得删掉
int a,b;
cin>>a>>b;
if(a>maxed&&b)//判断自身是否要输出 -1
//由于本题数据范围中说 a,b 均大于 1,所以 b 的判定条件也可以不写
{
cout<<-1;
return 0;
}
cout<<qpow(a,b);//输出快速幂答案
return 0;
}
另外,由于今年数据 和去年一样 很水,貌似 像我一样 写得不太紧密也可以过?
二、解密 \(\text{decode}\)
本题是一道数论题,但笔者好菜,硬把它做成了二分(捂脸)
先来说正解
我们看题目,因为本题要求
\[\left\{ \begin{aligned} n & =p \times q \\ e & \times d=(p-1) \times (q-1) +1 \end{aligned} \right. \]而 \(n,e,d\) 已知,所以本题实际上是在考察 二元二次方程组转化乘一元二次方程并求解
而 \(ed=(p-1)(q-1)+1 \Rightarrow ed=pq-p-q+2\)
又因为 \(n=pq\),所以 \(q=\frac{n}{p}\).代入 \(ed=pq-p-q+2\) 中,得
\[ed = \frac{np}{p} - \frac{n}{p}-p+2 \\ \Rightarrow ed=n-\frac{n}{p}-p+2 \\ \]继续下去,得出
\[\frac{n}{p}+p+ ed-n-2 =0 \\ \Rightarrow p-(n-ed+2)+\frac{n}{p}=0 \\ \]最后得到
\[p^2-(n-ed+2)p+n=0 \\ \]转化为一元二次方程后,我们考虑配方(令 \(n-ed+2=m\))
两边同时乘 \(4\),得
\[4 p^2 - 4mp+4n=0 \]接下来按照课本经验,我们凑数得出
\[4p^2-4mp+m^2=m^2-4n \]套用完全平方公式 \(a^2 \pm 2ab +b^2 = (a \pm b)^2\),得
\[(2p-m)^2=m^2-4n \]因为运算过程中 \(n,e,d,p,q,m\) 均为整数(\(n,e,d,p,q\) 题目描述中给出并限制,\(m\) 式在数据范围中给出并限制的)且 \(p<q\)
所以把 \(m=n-ed+2\) 代入,得
\[2p=-\sqrt{(n-ed+2)^2-4n} +(n-ed+2) \Rightarrow p=\frac{\sqrt{(n-ed+2)^2-4n}}{2} +(n-ed+2) \]同理,因为 \(p<q\),得出这个 二元二次方程组的解 为
\[\left\{ \begin{aligned} p &= n-ed-\sqrt{(n-ed+2)^2-4n}+2\\ q &= n-ed+\sqrt{(n-ed+2)^2-4n}+2\\ \end{aligned} \right. \](之所以不把 \((n-ed+2)^2\) 拆开是因为它们都是常数,应用到程序中好算)
接下来就是代码了:
#include<bits/stdc++.h>
#define endl "\n"
#define int long long
using namespace std;
int n,d,e,m,p,q;
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
freopen("decode.in","r",stdin);
freopen("decode.out","w",stdout);
int T;
cin>>T;
while(T--)
{
cin>>n>>d>>e;
m=(n-e*d+2)*(n-e*d+2)-(n<<2);
p=((n-e*d-(int)(round(sqrt(m)))+2)>>1);
q=((n-e*d+(int)(round(sqrt(m)))+2)>>1);//照上面的数学公式直接莽
//要注意的是,这里为了防止误差,从直接写 sqrt(m) 变成了 (int)(round(sqrt(m))),后面再加判断即可
if(p>0&&q>0&&n==p*q&&e*d==(p-1)*(q-1)+1) cout<<p<<" "<<q<<endl;//如果符合题目条件就输出 p,q
else cout<<"NO"<<endl;//不然就输出 NO
}
return 0;
}
另外我的写法是 二分,数学含量不高,但可能要废一些时间来调试二分边界
具体的,二分 \(p\) ,根据 \(p\) 算出 \(q=n-ed+2-p\),再看是否满足 \(pq=n\)
如果 \(pq<n\) ,那下次 \(p\) 就大一点;不然下次 \(p\) 就小一点
最后再判断一下,如果可行,那 \(l\) 就是 \(p\),\((n-ed+2-l)\) 就是 \(q\)
\(\text{Code:}\)
#include<bits/stdc++.h>
#define endl "\n"
#define int long long
using namespace std;
int n,d,e,m;
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
freopen("decode.in","r",stdin);
freopen("decode.out","w",stdout);
int T;
cin>>T;
while(T--)
{
cin>>n>>d>>e;
m=n-e*d+2;//算出 m
int l=1,r=m>>1;
while(l<r)//注意二分边界
{
int mid=l+((r-l)>>1);//二分
if(mid*(m-mid)<n) l=mid+1;
else r=mid;
}
if(l*(m-l)!=n) cout<<"NO"<<endl;//判断,如果不可行就输出 NO
else cout<<l<<" "<<m-l<<endl;//如果可行就按题解中说的输出 l 和 m-l
}
return 0;
}
三、逻辑表达式 \(\text{expr}\)
如果我没记错的话之前 \(CSP\) 就十分喜欢考字符串,以前也考过一道题叫 \(\text{expr}\) 的……
一般来说,处理中缀表达式所用的数据结构要么是栈,要么是树,两者本质上是一样的
但树的话比较麻烦,本题仅仅用栈就可以解决
具体的,对于读入的字符串,我们先在它的两头加一个括号 \(\left( \right)\) 表示开头和结束边界,再处理
对于字符串的每个字符:
如果是数字 \(0,1\),那么我们就用一个栈(为了方便说明,我们把它命名为值栈,里面存储的是后缀表达式运算的值)往里面压入这个数字
如果是左括号 \((\),那么我们就用另一个栈(为了方便说明,我们把它命名为符栈,里面存储的是各种运算符:(,&,),|
)往里面压入 \(-1\),表示从栈顶到这里为止,这里是左括号
如果是或运算符 |
,处理就比较复杂了
首先,我们要把之前从符栈栈顶到自顶到底的第一个 \(-1\),即左括号的位置的所有运算符进行计算,每次计算(与运算或或运算)从值栈栈顶和栈顶的后一位取出两个数字,运算后放回值栈的顶端。就这样,把符栈清空一段
接着我们往符栈栈顶压入 \(1\)(值可以自己定,但要方便程序区分),代表压入了一个或运算符
最后,我们考虑短路的问题。因为 \(1\) 或上任何东西它都是 \(1\),所以我们看值栈的顶端:如果它是 \(1\),就代表它短路了因此要 不停(与与运算符区分) 进行“跳跃”,即从当前位置,向字符串末尾跳过一整个括号序列,因为这一长串值是什么与这个表达式无关了,直到跳到该或运算符所在的括号序列的末端或者又遇到一个或运算符为止
当然,如果不短路就啥都不用做,短路时不要忘记把或运算符的短路计数器加 \(1\)
如果是与运算符 \(\&\),也向或运算符一样, 处理之前一段括号序列的值,再往符栈中压入 \(2\),代表压入了一个与运算符
再考虑短路的情况,与或运算符不同,与运算符只需要“跳跃”一次即可
由于篇幅所限
笔者摆烂力,读者可以自行思考为什么,或找出一组 \(\texttt{Hack}\) 说明
同样的,与运算符的短路计数器不要忘记加 \(1\)
最后,如果是右括号 \()\),就把符栈中积压的这一整串括号序列全部消完即可
最后,值栈的栈顶就是表达式的值,短路次数的话短路计数器已经统计过了
放上来之不易的代码:
#include<bits/stdc++.h>
// #define endl "\n"
using namespace std;
string str;
stack<bool>st_val;
stack<int>st_opt;
pair<int,int>shor;
inline void work()//计算值栈栈顶和栈顶下面两个值
{
int opt=st_opt.top();
st_opt.pop();
bool x,y;
x=st_val.top(),st_val.pop();
y=st_val.top(),st_val.pop();
if(opt==1) st_val.push(x|y);
else if(opt==2) st_val.push(x&y);
return;
}
inline int nex(int np)//用来“跳跃”的函数
{
if(str[np]==48||str[np]==49) return np;
if(str[np]=='(')
{
int top=0;
for(;np<str.size();np++)
{
if(str[np]=='(') top++;
else if(str[np]==')')
{
top--;
if(!top) return np;
}
}
}
return 1919810;//Homo 特有的返回值(喜)
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
freopen("expr.in","r",stdin);
freopen("expr.out","w",stdout);
cin>>str,str="("+str+")";//先把表达式左右两边套上一层括号
for(int i=0;i<str.size();i++)
{
if(str[i]==48) st_val.push(0);
else if(str[i]==49) st_val.push(1);//如果是数字直接往值栈里面压入
else if(str[i]=='(') st_opt.push(-1);//如果是左括号往符栈里面压入即可
else if(str[i]=='|')//如果是 |
{
while(!st_opt.empty()&&st_opt.top()>0) work();//先清空一段括号序列中表达式的值
st_opt.push(1);//压入标记
if(st_val.top())//满足短路条件
{
shor.second++;//或运算符短路计数器加 1
int point=i;
while(114514)//Homo 特有的判定条件(喜)
{
point=nex(point+1);
if(str[point+1]==')'||str[point+1]=='|') break;
else point++;
}
i=point,st_opt.pop();//跳跃,删除这个或符号的标记(已经运算过了)
}
}
else if(str[i]=='&')//如果是 &
{
while(!st_opt.empty()&&st_opt.top()>1) work();//先清空一段括号序列
st_opt.push(2);//压入与运算符的标记
if(!st_val.top()) shor.first++,i=nex(i+1),st_opt.pop();//同或运算符,但这次只需要“跳跃” 1 次
}
else if(str[i]==')')//如果是 )
{
while(!st_opt.empty()&&st_opt.top()>-1) work();//先清空该括号所包含的括号序列
st_opt.pop();//再删去一个
}
}
cout<<st_val.top()<<endl<<shor.first<<" "<<shor.second;//输出答案
return 0;
}
四、上升点列 \(\text{point}\)
我们看这道题 \(n \le 500\),可以想到时间复杂度 \(O(n^3)\) 的算法
第一种解法:动态规划
我们先以横坐标为第一关键字,纵坐标为第二关键字对整个点的序列排序
排序后,用 \(f_{i,j}\) 表示以点 \(i\) 结尾,自由点有 \(j\) 个的最长上升序列长度
我们考虑用三重循环:第一重循环枚举 \(i\),第二重循环枚举 \(j\),第三重循环用来转移
具体的,对于每个 \(i\) 从 \(1 \sim n\) 以及 \(j\) 从 \(0 \sim k\) ,都有 \(f_{i,j}\) 初始状态为 \(j+1\) (很容易理解:有 \(j\) 个自由点,再加上第 \(i\) 个一共 \(j+1\) 个)
然后看 \(i\) 点和 \(j\) 点之间够不够转移。如果够的话就转移
具体细节看代码吧,这里说不明白
#include<bits/stdc++.h>
#define endl "\n"
using namespace std;
int n,m,ans;
int f[509][109];
pair<int,int>a[509];
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
freopen("point.in","r",stdin);
freopen("point.out","w",stdout);
cin>>n>>m;//因为动态规划循环中要用 k,所以把输入中的 k 换成 m
for(int i=1;i<=n;i++) cin>>a[i].first>>a[i].second;
sort(a+1,a+n+1);//pair 默认以 first 为第一关键字,second 为第二关键字排序
for(int i=1;i<=n;i++)
for(int j=0;j<=m;j++)
{
f[i][j]=j+1;//先赋值初始状态(最小值)
for(int k=1;k<i;k++)
{
int dis=a[i].first-a[k].first+a[i].second-a[k].second;//算出它们距离
if(a[k].second>a[i].second||!dis||j+1<dis) continue;
//1. 方向错了(a[k].second>a[i].second) : continue
//2. 两点重合(!dis) : continue
//3. 不够搭上(j+1<dis) : continue
f[i][j]=max(f[i][j],f[k][j-dis+1]+dis);//更新
//枚举 k(中转点):1~k 用 j-dis+1 个自由点,(k+1)~i 用 dis 个
}
ans=max(ans,f[i][j]+m-j);//更新答案
}
cout<<ans;
return 0;
}
第二种解法:最短路 \(\text{Floyd}\)
和动态规划思想差不多,也是根据 \(\texttt{Floyd}\) 中的 \(k\) 中转点进行转换
只不过需要先建边
(事实上 \(\texttt{Floyd}\) 本质上就是动态规划)
\(\text{Code:}\)
#include<bits/stdc++.h>
#define endl "\n"
#define int long long
using namespace std;
int n,k,a[509][2];
int gra[509][509],res[509][509];
int ans;
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
//freopen("point.in","r",stdin);
//freopen("point.out","w",stdout);
memset(res,0x3f,sizeof(res));
cin>>n>>k;
for(int i=1;i<=n;i++) cin>>a[i][0]>>a[i][1];//Floyd 不需要排序
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(a[j][0]>=a[i][0]&&a[j][1]>=a[i][1])
res[i][j]=gra[i][j]=a[j][0]-a[i][0]+a[j][1]-a[i][1]-1;//建边,去掉首尾
for(int k=1;k<=n;k++)//枚举中转点(作用在解法一中说了)
for(int i=1;i<=n;i++)
{
if(i==k) continue;
for(int j=1;j<=n;j++)
{
if(i==j||j==k) continue;
res[i][j]=min(res[i][j],res[i][k]+res[k][j]);//Floyd 松弛方程
}
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(res[i][j]<=k) ans=max(ans,gra[i][j]+k-res[i][j]);//和解法一一样统计答案
cout<<(ans+=2);//最后再补上首尾两端的两个点
return 0;
}
第三种解法:记忆化搜索
把第一种解法动态规划稍微改变一下就可以变成记忆化搜索,这里不再过多阐述
代码:
#include<bits/stdc++.h>
#define endl "\n"
using namespace std;
int n,m,ans;
int a[1009][2];
int f[1009][1009];
inline int dfs(int p,int s)
{
if(~f[p][s]) return f[p][s];
int res=s;
for(int i=1;i<=n;i++)
{
if(a[i][0]<a[p][0]||a[i][1]<a[p][1]) continue;
int dis=a[i][0]-a[p][0]+a[i][1]-a[p][1];
if(!dis||s+1<dis) continue;
res=max(res,dfs(i,s-dis+1)+dis);
}
return f[p][s]=res;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
freopen("point.in","r",stdin);
freopen("point.out","w",stdout);
memset(f,-1,sizeof(f));
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i][0]>>a[i][1];
for(int i=1;i<=n;i++) ans=max(ans,dfs(i,m));
cout<<++ans;
return 0;
}