Loading

圣遗物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

不会使绝对值之和改变,木大木大木大(

然后不会了(

posted @ 2023-01-24 17:42  hsaht2426  阅读(24)  评论(0)    收藏  举报  来源