圣遗物2(《算法竞赛进阶指南》第一章 基本算法 整理)
1. 位运算(相关内容)
知识一:拆位
例一:acw89
考虑分治,因为是板子(?
(可能是因为 a^2n=(a^n)^2与乘方本身的递推关系,即f(2^i)可以O(log n)求出?)
复杂度O(log n)。
代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll mod;
ll fpow(ll a,ll p){
ll res=1%mod;
while(p){
if(p&1) res*=a,res%=mod;
p/=2;
a=smul(a,a);
a%=mod;
}
return res;
}
int main(){
ll a,b;
cin>>a>>b>>mod;
cout<<fpow(a,b);
return 0;
}
例二:acw90
本题核心在于将乘法操作转化为加法操作。
同理,有2an=an+an。
复杂度O(log n)。
代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll mod;
ll smul(ll a,ll p){
ll res=0;
while(p){
if(p&1) res+=a,res%=mod;
p/=2;
a+=a;
a%=mod;
}
return res;
}
int main(){
ll a,b;
cin>>a>>b>>mod;
cout<<smul(a,b);
return 0;
}
例三:acw998。
因为位运算不影响其他位,所以考虑拆位。
暴力处理出每一位为0/1后该位的值, 设其为res[i][0/1],复杂度为O(n log m)。
然后考虑m限制:
因为2^n>2^0+...2^(n-1),所以从高位到低位贪心。
考虑第i位:
(i) res[0]<res[1] 选f[0]
(ii) res[0]>res[1] 若可选f[1]则选,否则选f[0]
(iii) res[0]==res[1] 选f[0],消耗更少
贪心复杂度为O(n)。
总复杂度O(n log m)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int W=30+10,N=1e5+10;
int n,m,p[N][W],opt[N],res[W][2];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
string s;
int x;
cin>>s;
cin>>x;
for(int j=0;j<30;j++) p[i][j]=((x&(1<<j))>0);
if(s=="AND") opt[i]=0;
if(s=="OR") opt[i]=1;
if(s=="XOR") opt[i]=2;
}
for(int i=0;i<30;i++){
int x=0,y=1;
for(int j=1;j<=n;j++){
if(opt[j]==0) x&=p[j][i],y&=p[j][i];
if(opt[j]==1) x|=p[j][i],y|=p[j][i];
if(opt[j]==2) x^=p[j][i],y^=p[j][i];
}
res[i][0]=x,res[i][1]=y;
}
int ans=0,dam=0;
for(int i=29;i>=0;i--){
if(res[i][1]>res[i][0]&&m>=ans+(1<<i)) ans+=(1<<i),dam+=(res[i][1]<<i);
else dam+=(res[i][0]<<i);
}
cout<<dam;
return 0;
}
总结:此类题目多含位运算(或与2^n相关)/快速幂或龟速乘模板。
知识二:状态压缩
例—:acw95
直接判断似乎不易求解。
共25个位置,就有2^25个状态,可以bfs预处理出可达状态。
直接处理常数极大,会超时,考虑二进制压缩减少常数。
复杂度为O(n + 2^25)。
代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
unordered_map<int,int> mp;
queue<ll> q;
int a[5][5];
void init(){
int st=(1<<25)-1;
mp[st]=0;
q.push(st);
while(!q.empty()){
int now=q.front();
q.pop();
// cout<<mp[now]<<' '<<now<<'\n';
for(int i=0;i<25;i++){
int nxt=now;
nxt^=(1<<i);
if(i>=5) nxt^=(1<<(i-5));
if(i<20) nxt^=(1<<(i+5));
if(i%5>0) nxt^=(1<<(i-1));
if(i%5<4) nxt^=(1<<(i+1));
if(mp.find(nxt)==mp.end()){
mp[nxt]=mp[now]+1;
if(mp[nxt]==6) continue;
q.push(nxt);
}
nxt^=(1<<i);
if(i>=5) nxt^=(1<<(i-5));
if(i<20) nxt^=(1<<(i+5));
if(i%5>0) nxt^=(1<<(i-1));
if(i%5<4) nxt^=(1<<(i+1));
}
}
}
int main(){
init();
int t;
cin>>t;
while(t--){
long long val=0;
for(int i=1;i<=25;i++){
char c;
cin>>c;
val=val*2+(c-'0');
}
// cout<<val<<"\n";
if(mp.find(val)==mp.end()) cout<<"-1\n";
else cout<<mp[val]<<'\n';
}
return 0;
}
例二:acw91
因为n<=20,因此可以2^n状态压缩。
设f[i][j]代表状态为i时,此时在点j时的最短路径。(当然这就是状压dp的板子)(j是为了方便转移的)
所以有: f[i][j]=min(f[i][j],f[i^(1<<k)][k]+a[k][j])(i&(1<<k))
(
状压dp常用位运算:
(i) (i>>k)&1=i的第k位
同理有:
i|=(1<<k),i&=((1<<n)^(1<<k)),i^=(1<<k)
(ii) i&((1<<k)-1)=i的第0~k-1位
)
初值:f[0][0]=0,其他的赋极大值。
答案即为f[(1<<n)][n]。
复杂度为O(n * 2^n)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=20;
int n,dis[N+100][N+100],f[(1<<N)+10][N+10];
int main(){
cin>>n;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
cin>>dis[i][j];
memset(f,0x7f,sizeof(f));
f[1][0]=0;
for(int i=1;i<(1<<n);i++)
for(int j=0;j<n;j++)
for(int k=0;k<n;k++){
if(j==k) continue;
if((i&(1<<j))&&(i&(1<<k))){
f[i][j]=min(f[i^(1<<j)][k]+dis[k][j],f[i][j]);
}
}
cout<<f[(1<<n)-1][n-1];
return 0;
}
总结:此类题目数据范围多为n<=18~25.
2.递推(dp)与递归
简单来说,递归即通过调用自身解决子问题,递推即先求子问题,再用子问题的解来求出原问题。
知识一:线性递推
例一:acw95
我们换一种视角,一次操作只会影响上方的一个位置,中间的三个位置,下方的一个位置。
控制变量:考虑没有上方的第一列。
此时影响第一行的因素只会有第一行的操作与第二行的操作。
于是我们枚举第一行的操作,然后推算出第二行的操作即可。
对于第二行,已经做完了前两行的操作,即可递推。
代码:
用暴力水过去了所以没写喵。
喵喵喵
呜呜呜
例二:acw96
三塔时,我们有一个大家熟知的dp做法。
设f[n]代表n个圆盘的方案数。
对于第n个圆盘,我们要将其从A柱移到C柱,所以前n-1个就应从A移至B,在从B移至C。
所以有d[n]=2 * d[n-1] +1。
四塔时,我们可以先让i个盘到B塔或者C塔,发现剩下的圆盘仍可移动,所以剩下的n-i盘在3塔情况下移到D。
再将i盘在4塔(因为D塔都是更重的盘)情况下移动到D。
因此推导为 f[n] = min( f[n] , 2* f[i] + d[n - i] ) (1 <= i < n)
初值f[1]=0
复杂度O(n).
代码:
#include<bits/srdc++.h>
using namespace std;
#define ll long long
#define db double
const int N=12;
ll n,f[N+100],g[N+100];
int main(){
n=12;
for(int i=1;i<=n;i++) f[i]=f[i-1]*2+1;
for(int i=1;i<=n;i++){
g[i]=f[i];
for(int j=1;j<i;j++)
g[i]=min(g[i],2*g[j]+f[i-j]);
cout<<g[i]<<" "<<'\n';
}
return 0;
}
判断此类题目须找规律等,dp一章再研究。
知识二:分治
例一:acw97
看到因数和,套路地分解质因数。
于是题目变为n<=10^9等比数列含模数求和(多测
考虑是否可以用f(n)求f(2 * n),f(n + 1).
易得f(n+1)=f(n)+a^(n+1) , f(2 n)=f(n) (a^n+1)
于是在求f(n)时记录a^n即可。
复杂度O(log n)
用逆元水过去了所以没写喵。
汪汪汪。
例二:acw98
典型的分治求分形。
观察第n个图形与第n-1个图形。
于是可以得到。
(i)左上为原图沿左上-右下对角线翻转的
(ii)右上为原图
(iii)左下,右下分别为原图的上下镜像
这些特点与题目问题促使我们想到建立坐标系。
复杂度O(log n)
细节见代码:(以第一个点为(0,0)更简)
#include<bits/stdc++.h>
using namespace std;
#define ll long long
pair<ll,ll> sol(ll n,ll p){
if(n==1){
if(p==1) return make_pair(1,1);
else if(p==2) return make_pair(2,1);
else if(p==3) return make_pair(2,2);
else return make_pair(1,2);
}
ll m=(1<<(n-1));
ll num=m*m;
if(p<=num){
pair<ll,ll> res=sol(n-1,p);
swap(res.first,res.second);
return res;
}
else if(num<p&&p<=2*num){
pair<ll,ll> res=sol(n-1,p-num);
res.first+=m;
return res;
}
else if(2*num<p&&p<=3*num){
// cout<<"XXXXX"<<" ";
pair<ll,ll> res=sol(n-1,(4*num+1-p)-num);
// cout<<(4*num+1-p)-num<<" "<<res.first<<" "<<res.second<<'\n';
res.first+=m,res.second=2*m+1-res.second;
return res;
}
else{
pair<ll,ll> res=sol(n-1,4*num+1-p);
swap(res.first,res.second);
res.second=2*m+1-res.second;
return res;
}
}
int main(){
int t;
cin>>t;
while(t--){
ll n,a,b;
cin>>n>>a>>b;
pair<ll,ll> xa=sol(n,a),xb=sol(n,b);
// cout<<xa.first<<" "<<xa.second<<"\n";
// cout<<xb.first<<" "<<xb.second<<"\n";
cout<<(ll)(10*sqrt(1.0*(xa.first-xb.first)*(xa.first-xb.first)+
1.0*(xa.second-xb.second)*(xa.second-xb.second))+0.5)<<"\n";
}
return 0;
}
例三:acw119
待补充。
此类题目多为模板。
3.前缀和与差分
若对于每个s[i],有s[i]=a[1]+a[2]+..a[i], 即a[i]=s[i]-s[i-1].
即称s为a的前缀和,a为s的差分。
知识一:直接运用
例一:acw99
暴力的想法是枚举左端点然后暴力计算矩阵大小。
二维前缀和优化即可。
坑: (i) r可能大于x,y范围,应使r=min(r,5001)
P.S.无论如何应考虑越界问题。
(ii) 不同目标可能在同一位置。
P.S.慎用+=。
复杂度O(5000 * 5000).
二维前缀计算见代码:
#include<bits/stdc++.h>
using namespace std;
const int N=5e3+10;
int n,r;
int x,y,w;
int s[N][N],ans;
int main(){
cin>>n>>r;
r=min(r,5001);
for(int i=1;i<=n;i++) cin>>x>>y>>w,s[x+1][y+1]+=w;
for(int x=1;x<=5001;x++)
for(int y=1;y<=5001;y++)
s[x][y]+=s[x][y-1]+s[x-1][y]-s[x-1][y-1];
for(int i=1;i<=5001-r+1;i++)
for(int j=1;j<=5001-r+1;j++){
int w=s[i+r-1][j+r-1]-s[i-1][j+r-1]-s[i+r-1][j-1]+s[i-1][j-1];
ans=max(ans,w);
}
cout<<ans;
return 0;
}
例二:acw100
操作为区间加减,考虑差分。
原操作变为c[l]+1,c[r+1]-1或c[l]-1,c[r+1]+1。
那么题目要求的最终状态即c[2]~c[n]全部为零。
考虑最优情况,假设所有要变的c需要的次数都最小,即abs(c[i]).
因此每步都需使abs(c[i])减少。
尝试构造:
我们考虑操作的坐标值在1,n+1之间,于是可将操作分为以下四种:
(i)l>1&&r<n
此时操作会使两个数分别加减1。
使用一正一负,绝对值之和每次减少2。
(ii)l=1&&r<n
此时操作会使一个数加或减1。
绝对值之和每次减少1。
(iii)l>1&&r=n
此时操作会使一个数加或减1。
绝对值之和每次减少1。
(iiii)l=1&&r=n
不会使绝对值之和改变,木大木大木大(
然后不会了(

浙公网安备 33010602011771号