最近两场cf总结

博主分享了在Codeforces平台参加的两场比赛的总结,详细分析了每道题目的解题思路,涉及暴力模拟、动态规划、数学算法、前缀和DP、树形结构、线段树等多种算法。通过比赛,博主强调了代码能力和数学思维的重要性,并提供了部分题目的链接以供深入学习。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近做了两场cf,通过了当中所有的题目,稍稍做下总结
Codeforces Round #325 (Div. 1)
这场是我排名最靠前的一次(23),虽然04写的比较慢,但是幸运的猜出了03,05和06都是短代码的可做题,然而并没有时间去看,这说明代码能力还是很重要的
A:有n个小孩依次去看牙齿,每个小孩看的时候会对后面的小孩造成等差递减的伤害,假如小孩不能承受这个伤害就会逃跑,问最后有几个小孩去看了牙齿,n<=4000
分析:比较简单的题目,暴力模拟每个小孩的看牙情况,用一个数组记录小孩是否还在队列当中

B:给出一张3*n的地图,有些位置有列车,列车每次向左行驶两格,行人最初在第一列,每次向右走一格,之后可以决定向上或者向下走一格(不超出地图),问行人最后是否有可能到达最后一列。
分析:可以转换坐标系,变成行人每次向右走三格之后决定向上或向下,之后就是一个简单的dp

C:A最初只有一个苹果,B最初只有一个橘子,现在要你构造一个01串操作序列,假如操作是0,则B拿A当前所具有苹果和橘子,否则A对B做相同的事,要求最后A和B的苹果数之和为x,橘子数之和为y.( x,y<=1018 )

分析:是一道比较巧妙的数学题,比赛的时候用欧几里德算法带进去算了一下,正好满足!于是就通过了这题
题解给了一个不为人知的数据结构:https://en.wikipedia.org/wiki/Stern%E2%80%93Brocot_tree

D:D是一个挺无聊的折半暴搜,虽然很简单,但是还是码了我一个小时之久,主要是一开始题目看错了。。。。

E:n个数,从中选出一个非空集合,在另外选出一个数,要求集合的gcd不为1且这个gcd和另外选择的数互质

分析:比较常用的筛法,假如把题目变成选择两个不同的数,要求gcd为1,那么我们可以先求 cnt[x]x ,筛一遍之后,我们可以先忽略不同这个要求,那么gcd是x的倍数的选法就是 cnt[x]2 ,之后再套用莫比乌斯函数暴力容斥即可得到答案,最后再减去1的数目即可。
对于这道题,也类似搞搞,不同之处在于他要求所选集合gcd不为1,因此我们最后再剪掉选择集合gcd==1形成的方案数,而后面也可以用莫比乌斯暴力容斥
代码极短

#include<bits/stdc++.h>
using namespace std;
const int Maxn=10000002,M=1e9+7;
typedef long long Int;
int miu[Maxn],cnt[Maxn];
int n;
int xp[500020];
int main()
{
    xp[0]=1;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)xp[i]=(xp[i-1]+xp[i-1])%M;
    for(int i=1;i<=n;i++)
    {
        int x;scanf("%d",&x);
        cnt[x]++;
    }
    int ans=0;
    miu[1]=1;
    for(int i=1;i<Maxn;i++)
    {
        for(int j=i+i;j<Maxn;j+=i)
        {
            miu[j]-=miu[i];
            cnt[i]+=cnt[j];
        }
        if(!cnt[i]||!miu[i])continue;
        ans+=miu[i]*(xp[cnt[i]]-1)*(Int)(cnt[i]-n)%M;
        if(ans>=M)ans-=M;
        if(ans<0)ans+=M;
    }   
    printf("%d\n",ans);
}

F:给一个长<1000的数字串,给两个长度均为d且不含前导0的L和R,要求L~R之间有多少个串满足至少有一个长度>=d/2的子串是原数字串的子串。d<=50
这道题比赛的时候并没有人通过这道题,当时也是被吓的题目都没读过。然而今天读了一下,发现难度比我想象的要低很多。L~R之间有多少个满足的,看这样的数据范围很容易想到数位dp,关键是状态,由于要求是原串的子串,那么我们只需要知道当前匹配的最长原串子串是怎么样的即可。很容易想到后缀自动机,由于长度只有1000,那么总结点数也只有2000,放在状态里面一点也不过分。当然,后缀自动机只知道在那个节点还并不能知道当前串长是多少,由于d只有50,我们可以在一维代表长度,转移的时候暴力枚举填充的数就可以通过这道题

#include<bits/stdc++.h>
using namespace std;
const int Maxn=2020,M=1e9+7;
string s;
int last,sz;
int ml[Maxn],f[Maxn],ch[Maxn][10];
void add(int c)
{
    int p=last;
    int np=++sz;
    ml[np]=ml[p]+1;
    last=np;
    for(;p&&!ch[p][c];p=f[p])ch[p][c]=np;
    if(!p){f[np]=1;return;}
    int q=ch[p][c];
    if(ml[q]==ml[p]+1){f[np]=q;return;}
    int nq=++sz;
    memcpy(ch[nq],ch[q],sizeof(ch[q]));
    ml[nq]=ml[p]+1;
    f[nq]=f[q];
    f[q]=f[np]=nq;
    for(;p&&ch[p][c]==q;p=f[p])ch[p][c]=nq;
}
inline void up(int &x,int y){x+=y;if(x>=M)x-=M;}
int dp[52][26][2020][2][2];
int dfs(int cur,int curlen,int curst,bool can,bool has)
{
    int &t=dp[cur][curlen][curst][can][has];
    if(cur>=s.size()){return t=has;}
    if(t>=0)return t;
    t=0;
    for(int i=0;i<10;i++)
    {
        if(!can&&i+'0'>s[cur])continue;
        bool ncan=(i+'0'==s[cur])?can:1;
        bool nhas=has;
        int nst,nlen;
        if(has)
        {
            nhas=1;nlen=0;
            nst=1;
            up(t,dfs(cur+1,nlen,nst,ncan,nhas));
        }
        else
        {
            if(ch[curst][i]){nst=ch[curst][i];nlen=curlen+1;}
            else
            {
                nst=curst;
                while(nst&&!ch[nst][i])nst=f[nst];
                if(!nst){nst=1;nlen=0;}
                else {nlen=ml[nst]+1;nst=ch[nst][i];}
            }
            if(nlen>=s.size()/2){nhas=1;nst=1;nlen=0;}
            //if(cur==1&&curlen==1&&i==2)printf("nlen=%d nst=%d ncan=%d nhas=%d\n",nlen,nst,ncan,nhas);
            up(t,dfs(cur+1,nlen,nst,ncan,nhas));
        }
    }
    return t;
}
int solve()
{
    memset(dp,-1,sizeof(dp));
    return dfs(0,0,1,0,0);
}
int main()
{
    cin>>s;
    last=sz=1;
    for(int i=0;i<s.size();i++)add(s[i]-'0');
    //printf("ch=%d\n",ch[2][2]);
    cin>>s;
    reverse(s.begin(),s.end());
    for(int i=0;i<s.size();i++)
    {
        if(s[i]=='0')s[i]='9';
        else {s[i]--;break;}
    }
    reverse(s.begin(),s.end());
    solve();
    int ans=(M-solve())%M;
    cin>>s;
    ans+=solve();if(ans>=M)ans-=M;
    printf("%d\n",ans);
}

Codeforces Round #326 (Div. 1)
这场就没有上一场那么幸运,可能和比赛时间有一定关系,当时就感觉眼睛都睁不开了,很难进行思考,02题目看错,在wa中度过了余生,03写了树链剖分和暴力合并,结果被卡掉了,仔细想想这两题任何一道题通过了名次都还不至于这么差。。。。

A:给出一个数列,每次可以删除一个 2a[i]=2x 的数列,问最少需要删几次。
分析:显然的贪心即可,因为只有某个数的个数>=2才可以向后删,因此就for一遍,当某个数出现次数>=2,就暴力去删一次,仔细考虑清楚细节即可

B:给出一个长为l周期为n的数列,从中选出长度<=k的数列,要求每个数出自连续的不同周期,问方案数。 nk<=106
分析:比较明显的前缀和dp,主要比赛的时候没看到连续,可怕的是还能过样例,所以说下次比较简单的题目一直wa就该去重新读一下题目!

C:给出一棵树,询问q次,每次询问树上的前K大(K<=10)
分析:显然用主席树求树上第K大毫无疑问可以通过这题,不过代码量有些大,不在考虑范围;因为只有10个,显然可以用线段树暴力合并,不用思考的做法就是外面套一层树链剖分就可以维护链上查询了,然而这个方法tle了;考虑到合并的复杂度显得有些大,我们可以只维护最小值,然后修改完之后查询,这样虽然复杂度没有变,但是常数变小了,有人这样通过了这题;正解是倍增,因为只有查询没有修改,用倍增维护10个值然后暴力合并,就可以少掉1个log,足以通过这题。

D:给出一张图,每条边有一个颜色和费用,要求删去一个没有公共点的边集,使得剩下的相同颜色的边无公共点,且最大的费用最小。
分析:最大费用最小,就是在提醒我们二分答案,之后就转化成某些边一定要保留,能否1.每个点至多删去一条边,使得2.剩下的相同颜色的边无公共点。根据这两个限制进行建图,由于第一个条件可能会导致边数爆炸,因此我们可以将每个点排序,对每个前缀新建一个节点,对每个后缀新建一个节点,之后再连边边数就变成O(n)了,之后再暴力跑2sat,由于有些边一定要保留,就先对那些边对应的变量跑一遍dfs,剩下的仍然是一个对称图,可以使用O(n)的强联通分量算法,当然,我还是写了暴力dfs,实际效果跑得也非常快,注意2-sat的连边方式:A->B则连A到B和~B到~A两条边

#include<bits/stdc++.h>
using namespace std;
const int Maxn=50020,Inf=1e9+7;
int n,m,tot;
int tl[Maxn],col[Maxn];
int uu[Maxn],vv[Maxn];
bool cmp(int a,int b){return col[a]<col[b];}
vector<int>G[Maxn];//edge in vertix
vector<int>G2[Maxn<<4];//edge in two-sat,x<<1:del,x<<1|1:not del
vector<int>rep;
bool mark[Maxn<<4];
int top,S[Maxn<<4];
bool dfs(int u)
{
    if(mark[u^1])return 0;
    if(mark[u])return 1;
    mark[u]=1;
    S[top++]=u;
    for(int i=0;i<G2[u].size();i++)
    {
        if(!dfs(G2[u][i]))return 0;
    }
    return 1;
}
bool check(int Tl,int ptmod=0)
{
    top=0;
    for(int i=0;i<tot<<1;i++)mark[i]=0;
    //for(int i=0;i<G2[0].size();i++)printf("ok%d ",G2[0][i]);puts("");
    for(int i=0;i<m;i++)
    {
        if(tl[i]>Tl)//bixubaoliu
        {
            if(mark[i<<1])return 0;
            if(mark[i<<1|1])continue;
            if(!dfs(i<<1|1))return 0;
        }
    }
    for(int i=0;i<m;i++)
    {
        if(!mark[i<<1]&&!mark[i<<1|1])
        {
            if(!dfs(i<<1|1))
            {
                while(top)mark[S[--top]]=0;
                if(!dfs(i<<1))return 0;
                if(ptmod)rep.push_back(i);
            }
        }
        else if(mark[i<<1]&&ptmod)rep.push_back(i);
    }
    if(ptmod)
    {
        puts("Yes");
        printf("%d %d\n",Tl,(int)rep.size());
        for(int i=0;i<rep.size();i++)printf("%d%c",rep[i]+1,i==rep.size()?'\n':' ');
    }
    return 1;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=0;i<m;i++)
    {
        scanf("%d%d%d%d",uu+i,vv+i,col+i,tl+i);
        G[uu[i]].push_back(i);
        G[vv[i]].push_back(i);
    }
    bool flag=1;
    tot=m;
    for(int i=1;i<=n&&flag;i++)
    {
        vector<int>&V=G[i];
        sort(V.begin(),V.end(),cmp);
        for(int j=0,k;j<V.size();j=k)
        {
            vector<int>tp;
            for(k=j;k<V.size()&&col[V[k]]==col[V[j]];k++)tp.push_back(V[k]);
            if(tp.size()>2){flag=false;break;}
            if(tp.size()==2)
            {
                G2[tp[0]<<1|1].push_back(tp[1]<<1);
                G2[tp[1]<<1|1].push_back(tp[0]<<1);
            }
        }
        vector<int>pre,suf;
        for(int j=0;j<V.size();j++)
        {
            int t=tot++;
            pre.push_back(t);
            G2[t<<1].push_back(V[j]<<1);G2[t<<1|1].push_back(V[j]<<1|1);
            if(j)G2[t<<1].push_back((t-1)<<1),G2[t<<1|1].push_back((t-1)<<1|1);
        }
        for(int j=V.size()-1;j>=0;j--)
        {
            int t=tot++;
            suf.push_back(t);
            G2[t<<1].push_back(V[j]<<1);G2[t<<1|1].push_back(V[j]<<1|1);
            if(j<V.size()-1)G2[t<<1].push_back((t-1)<<1),G2[t<<1|1].push_back((t-1)<<1|1);
        }
        reverse(suf.begin(),suf.end());
        for(int j=0;j<V.size();j++)
        {
            int t=V[j];
            if(j)G2[t<<1].push_back(pre[j-1]<<1|1);
            if(j<V.size()-1)G2[t<<1].push_back(suf[j+1]<<1|1);
        }
    }
    //if(tot>=(Maxn<<4))while(1);
    //if(m>40000)return 0;
    if(!flag||!check(Inf)){puts("No");return 0;}
    if(check(0)){puts("Yes");puts("0 0");return 0;}
    int L=0,R=Inf;
    while(L+1<R)
    {
        int mid=(L+R)>>1;
        if(check(mid))R=mid;
        else L=mid;
    }
    check(R,1);
}

E:给一个序列,每次可以给L~R的数^K,或者询问L~R的数高斯消元后基的数量。
分析:一开始死脑筋想线段树维护区间的基,然而对于区间修改线段树并没办法维护一个集合^K之后的基。这里就有一个常用技巧,维护差分序列!由于要求基,那么相邻两个异或一遍再求并不影响结果,因此本题就变成单点修改的线段树暴力。类似的技巧还出现在上次维护区间gcd,然而这次碰到了还是没想到这个科技

#include<bits/stdc++.h>
using namespace std;
#define ls l,mid,x<<1
#define rs mid+1,r,x<<1|1
const int Maxn=200020;
vector<int>tree[Maxn<<2];
int n,q;
int a[Maxn],b[Maxn],c[Maxn];
inline int low(int x){return x&-x;}
int ask(int loc){int ret=0;for(int i=loc;i>=1;i-=low(i))ret^=c[i];return ret;}
int add(int loc,int w){for(int i=loc;i<=n;i+=low(i))c[i]^=w;}
void gauss(vector<int>&V)
{
    for(int i=0,j=0;i<32&&j<V.size();i++)
    {
        int cs=-1;
        for(int k=j;k<V.size();k++)
        {
            if(V[k]>>i&1){cs=k;break;}
        }
        if(cs<0)continue;
        if(cs!=j){swap(V[cs],V[j]);}
        for(int k=0;k<V.size();k++)
        {
            if(k!=j&&(V[k]>>i&1))V[k]^=V[j];
        }
        j++;
    }
    while(V.size()&&!V.back())V.pop_back();
}
vector<int> operator+(const vector<int>&V1,const vector<int>&V2)
{
    vector<int>ret=V1;
    for(int i=0;i<V2.size();i++)ret.push_back(V2[i]);
    gauss(ret); 
    return ret;
}

void build(int l,int r,int x)
{
    if(l==r)
    {
        scanf("%d",a+l);
        b[l]=a[l]^a[l-1];
        add(l,b[l]);
        if(b[l])tree[x].push_back(b[l]);
        return;
    }
    int mid=(l+r)>>1;
    build(ls);build(rs);
    tree[x]=tree[x<<1]+tree[x<<1|1];
}
void modify(int tar,int w,int l,int r,int x)
{
    if(l==r){b[l]^=w;add(l,w);if(tree[x].size())tree[x].pop_back();if(b[l])tree[x].push_back(b[l]);return;}
    int mid=(l+r)>>1;
    if(tar<=mid)modify(tar,w,ls);
    else modify(tar,w,rs);
    tree[x]=tree[x<<1]+tree[x<<1|1];
}
vector<int>query(int L,int R,int l,int r,int x)
{
    if(L>R){vector<int>tp;return tp;}
    if(L<=l&&R>=r)return tree[x];
    int mid=(l+r)>>1;
    vector<int>ret;
    if(L<=mid)ret=ret+query(L,R,ls);
    if(R>mid)ret=ret+query(L,R,rs);
    return ret;
}

int main()
{
    scanf("%d%d",&n,&q);
    build(1,n,1);
    while(q--)
    {
        int ty,l,r,k;
        scanf("%d%d%d",&ty,&l,&r);
        if(ty==1)
        {
            scanf("%d",&k);
            modify(l,k,1,n,1);
            if(r<n)modify(r+1,k,1,n,1);
        }
        else
        {
            vector<int>rep=query(l+1,r,1,n,1);
            rep.push_back(ask(l));
            gauss(rep);
            printf("%d\n",1<<rep.size());
        }
    }
}

F:给出n个串,q次询问,每次询问l,r,k,输出l~r中的串在第k个串中出现次数和,n,q<=10W,保证串长之和<=10w;

分析:快速询问A串在B串中出现的次数,很常规的做法就是建出fail树,然后把B所经过的节点标为1,然后查询A串终结节点所在子树的权值和即可。
对于这道题,套用类似的方法发现复杂度并不正确,看这种数据规模可以按照串长分块:
对于比较短的串,最多只有O(q)个询问,每次询问一个点到根的路径上有多少个权值位于L,R之间,显然这样的询问最多只有 O(Qn) 个,根据询问的性质,一个点上的权值只会对子树造成影响,因此我们可以考虑dfs的同时动态修改、询问当前每种权值的个数,考虑到询问的次数比较多,可以对权值也进行分块,就能使询问 O(1) ,修改 O(n) ,因此这一部分的复杂度就是 On
对于比较长的串,显然这样的串不会多于 O(n) 个,因此只要将询问离线,暴力询问每个串每个节点的子树有多少个目标串的节点即可

#include<bits/stdc++.h>
using namespace std;
typedef long long Int;
const int Maxn=200020;
int n,q,sz,last,len;
int ml[Maxn],f[Maxn],ch[Maxn][26];
vector<int>G[Maxn];
string ss[Maxn];
void add(int c)
{
    int p=last;
    if(ch[p][c])
    {
        int q=ch[p][c];
        if(ml[q]==ml[p]+1)last=q;
        else
        {
            int nq=++sz;
            memcpy(ch[nq],ch[q],sizeof(ch[q]));
            ml[nq]=ml[p]+1;
            last=nq;
            f[nq]=f[q];
            f[q]=nq;
            for(;p&&ch[p][c]==q;p=f[p])ch[p][c]=nq;
        }
    }
    else
    {
        int np=++sz;last=np;
        for(;p&&!ch[p][c];p=f[p])ch[p][c]=np;
        if(!p){f[np]=1;return;}
        int q=ch[p][c];
        if(ml[q]==ml[p]+1){f[np]=q;return;}
        int nq=++sz;
        memcpy(ch[nq],ch[q],sizeof(ch[q]));
        ml[nq]=ml[p]+1;
        f[nq]=f[q];
        f[q]=f[np]=nq;
        for(;p&&ch[p][c]==q;p=f[p])ch[p][c]=nq;
    }
}
void insert(string &s)
{
    last=1;
    for(int i=0;i<s.size();i++)
    {
        add(s[i]-'a');
    }
}
int ql[Maxn],qr[Maxn],qk[Maxn],block;
Int rep[Maxn],tp[Maxn],tpele[Maxn];
vector<int>Q1;
vector<int>Q2[Maxn];
vector<int>End[Maxn];
int pre[Maxn],fin[Maxn],dfs_t;
int cntblock[500],cntele[Maxn];
bool cmp(int a,int b){return qk[a]<qk[b];}
void process(int u)
{
    for(int i=0;i<=n;i++)tp[i]=0;
    for(int i=1;i<=sz;i++)tpele[i]=0;
    for(int i=0,j=1;i<ss[u].size();i++)
    {
        j=ch[j][ss[u][i]-'a'];
        tpele[pre[j]]++;
    }
    for(int i=1;i<=sz;i++)tpele[i]+=tpele[i-1];
    for(int i=1;i<=n;i++)
    {
        tp[i]=tp[i-1];
        int k=1;
        for(int j=0;j<ss[i].size();j++)
            k=ch[k][ss[i][j]-'a'];
        tp[i]+=tpele[fin[k]]-tpele[pre[k]-1];
    }
}
void solve1()
{
    sort(Q1.begin(),Q1.end(),cmp);
    for(int i=0,j;i<Q1.size();i=j)
    {
        int u=qk[Q1[i]];
        process(u);
        for(j=i;j<Q1.size()&&qk[Q1[j]]==u;j++)
        {
            int id=Q1[j];
            rep[id]+=tp[qr[id]]-tp[ql[id]-1];
        }
    }
}
void add(int loc,int w)
{
    loc--;
    int t=loc/block;
    for(int i=0;i<t;i++)cntblock[i]+=w;
    for(int i=t*block;i<=loc;i++)cntele[i]+=w;
}
int ask(int loc)
{
    loc--;
    if(loc>=n)return 0;
    return cntele[loc]+cntblock[loc/block];
}
void dfs(int u)
{
    pre[u]=++dfs_t;
    for(int i=0;i<End[u].size();i++)
    {
        add(End[u][i],1);
    }
    for(int i=0;i<Q2[u].size();i++)
    {
        int id=Q2[u][i];
        rep[id]+=ask(ql[id])-ask(qr[id]+1);
    }
    for(int i=0;i<G[u].size();i++)dfs(G[u][i]);
    for(int i=0;i<End[u].size();i++)add(End[u][i],-1);
    fin[u]=dfs_t;
}
int main()
{
    scanf("%d%d",&n,&q);
    last=sz=1;
    for(int i=1;i<=n;i++)cin>>ss[i],insert(ss[i]),len+=ss[i].size();
    len=sqrt(len+0.5);
    for(int i=1;i<=q;i++)
    {
        scanf("%d%d%d",ql+i,qr+i,qk+i);
        if(ss[qk[i]].size()>=len)
        {
            Q1.push_back(i);
        }
        else
        {
            for(int k=0,j=1;k<ss[qk[i]].size();k++)
            {
                j=ch[j][ss[qk[i]][k]-'a'];
                Q2[j].push_back(i);
            }
        }
    }
    block=sqrt(n+0.5);
    for(int i=1;i<=n;i++)
    {
        int k=1;
        for(int j=0;j<ss[i].size();j++)
        {
            k=ch[k][ss[i][j]-'a'];
        }
        End[k].push_back(i);
    }
    for(int i=1;i<=sz;i++)G[f[i]].push_back(i);
    dfs(1);
    solve1();
    for(int i=1;i<=q;i++)printf("%lld\n",rep[i]);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值