CodeFestival 2017 Final 题解

这篇博客详细解析了CodeFestival 2017决赛的多个问题,包括Problem A到J。涉及的解题策略涵盖枚举、动态规划、并查集、贪心算法、数论、矩阵变换等多种算法思想,通过实例和证明解释了各种问题的解决方案。

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

Problem A.(…)

#include<bits/stdc++.h>
using namespace std;
void dfs(int x,string a,string b,string c){
    if(x==a.length()){
        if(b==c){puts("YES");exit(0);}
        else return ;
    }
    if(a[x]=='A')dfs(x+1,a,b,c);
    dfs(x+1,a,b,c+a[x]);
}
int main(){
    string s,t="AKIHABARA";
    cin>>s;
    dfs(0,t,s,"");
    puts("NO");
}

Problem B.(…)

#include<bits/stdc++.h>
using namespace std;
char s[100100];
int a[100];
int main(){
    scanf("%s",s);
    for(int i=0;s[i];i++)
        a[s[i]-'a']++;
    int c=min(a[0],min(a[1],a[2]));
    for(int i=0;i<3;++i)
        if((a[i]-=c)>=2)return puts("NO"),0;
    puts("YES");
}

Problem C.
可以发现0~12的数每种只能变成d或24-d,枚举变成哪个即可,复杂度O(212122)

#include<bits/stdc++.h>
using namespace std;
int n,a[20],c[100],tp,ans;
int chk(){
    int ret=10007;
    for(int i=0;i<=tp;++i)
        for(int j=i+1;j<=tp;++j)
            ret=min(ret,min(abs(c[i]-c[j]),24-abs(c[i]-c[j])));
    return ret;
}
void dfs(int x){
    if(x==13){
        ans=max(ans,chk());
        return ;
    }
    if(a[x]==2)c[++tp]=x,c[++tp]=24-x,dfs(x+1),tp-=2;
    else if(a[x]==1)c[++tp]=x,dfs(x+1),c[tp]=24-x,dfs(x+1),tp--;
    else dfs(x+1);
}
int main(){
    scanf("%d",&n);
    for(int i=1,x;i<=n;++i)
        scanf("%d",&x),a[x]++;
    for(int i=0;i<=12;++i)
        if(a[i]>2)return puts("0"),0;
    dfs(0);
    printf("%d",ans);
}

Problem D.
可以证明,被选的人的Hi+Pi一定是递增的。
证明如下:假设有两个人a,b,那么a在b前面的条件就是min(Ha,HbPa)min(Hb,HaPb),即Ha+PaHb+Pb

于是可以拿一个dp(i,j)表示前i个人已经选了j个人时栈的最小高度。
(直接贪心是错的,因为不能将全部人选完)

#include<bits/stdc++.h>
#define maxn 5010
using namespace std;
struct people{
    int h,p;
    void scan(){scanf("%d%d",&h,&p);}
    int operator<(const people& a)const{
        return p+h<a.p+a.h;
    }
}A[maxn];
int n,ans;
long long dp[maxn];
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;++i)A[i].scan();
    sort(A+1,A+n+1);
    memset(dp,0x3f,sizeof(dp));
    dp[0]=0;
    for(int i=1;i<=n;++i)
        for(int j=i;j>=0;--j)if(dp[j]<=A[i].h)
            dp[j+1]=min(dp[j+1],dp[j]+A[i].p);
    for(int i=n;i>=1;--i)if(dp[i]!=0x3f3f3f3f3f3f3f3fll)return printf("%d",i),0;
} 

Problem E.
首先将整个序列差分,问题转化为“将一些位置+1/-1,使得(i,n+2i)和为26的倍数”

对于每种操作连边,再连上(i,n+2i),则问题等价于每个联通块的和为26的倍数。因为无论怎么+1/-1,每个联通块的和是不变的。

用并查集维护就可以了。

#include<bits/stdc++.h>
#define maxn 1000100
using namespace std;
int n,sum[maxn*2],tp,m,a[maxn],f[maxn];
char s[maxn];
int g(int x){
    return x<0?x+26:x;
}
int find(int x){
    return x==f[x]?x:f[x]=find(f[x]);
}
int main(){
    scanf("%s%d",s+1,&n);
    int m=strlen(s+1);
    for(int i=1;i<=m;++i)s[i]-='a',f[i]=i;
    f[m+1]=m+1;
    for(int i=m+1;i>=1;--i)s[i]=g(s[i]-s[i-1]);
    for(int i=1,l,r;i<=n;++i)
        scanf("%d%d",&l,&r),f[find(l)]=find(r+1);
    for(int i=1;i<=m-i+2;++i)f[find(i)]=find(m+2-i);
    for(int i=1;i<=m+1;++i)sum[find(i)]+=s[i];
    for(int i=1;i<=m+1;++i)if(i==find(i)&&sum[i]%26)return puts("NO"),0;
    puts("YES");
}

Problem F.
这个题。。。看题先打表。。。然后发现符合条件的N,K特别少。
可以观察一些小数据:

7 3
1 2 3
1 4 5
1 6 7
2 4 6
2 5 7
3 4 7
3 5 6

可以发现,它的构造方式为:
1.前K行开头为1,后面依次分配剩余元素。这只能在N=K(K1)+1才能办到。
2.后面[2,K]开头时,将后面分为K1块,每块每次等距地选,比如上图就是:

1:[2,3],[4,5],[6,7]
块:[4,5][6,7]
2:{1,1}{2,2}(相距0)
3:{1,2}{2,1}(相距1)

这只有在K1为质数时才能符合题目条件。

#include<bits/stdc++.h>
using namespace std;
int n,k,a[1000][100];
int main(){
    k=42,n=k*(k-1)+1;
    printf("%d %d",n,k);
    for(int i=1;i<=k;++i){
        printf("\n1 ");
        for(int j=(i-1)*(k-1)+2,p=1;p<=k-1;p++,j++)
            printf("%d ",j),a[i][p-1]=j;
    }
    for(int i=0;i<k-1;++i){
        for(int j=0;j<k-1;++j){
            printf("\n%d ",i+2);
            for(int l=2,ptr=j;l<=k;l++,ptr=(ptr+i)%(k-1))
                printf("%d ",a[l][ptr]);
        }
    }
}

Problem G.
考虑对于一个固定的序列a如何求答案。显然,可以用一个dp 表示最大操作次数:
dp(i)={(ai+nj=i+1dp(j))/i0aiiai>i
相当于是每次从左往右一遇到能选的就选,然后开始下一轮循环。
最终答案是ni=1aini=1dp(i)

考虑两边分别统计,每个元素在[0,K]之间,所以所有ai的和就是:
(K+1)n1×K(K+1)2×n=nK2×(K+1)n

如何求得所有情况dp的和呢?考虑用f(i,j)表示[i,n]的元素dp值的和为j的方案数。
每次枚举该元素是[0,K]的哪个数。
根据上面的转移很容易得到f(i,j)的转移。

可以发现j不是很大,所以复杂度很小。

#include<bits/stdc++.h>
#define mod 1000000007
using namespace std;
int dp[110][11000],n,k,sum;
int qpow(int a,int b){
    int ans=1,tmp=a;
    for(;b;b>>=1,tmp=1ll*tmp*tmp%mod)
        if(b&1)ans=1ll*ans*tmp%mod;
    return ans;
}
int main(){
    scanf("%d%d",&n,&k);
    dp[n+1][0]=1,sum=0;
    for(int i=n;i>=1;sum+=(k+sum)/i,--i)
        for(int j=0;j<=sum;++j)if(dp[i+1][j])
            for(int a=0;a<=k;++a)
                if(a>i)dp[i][j]=(dp[i][j]+dp[i+1][j])%mod;
                else dp[i][j+(a+j)/i]=(dp[i][j+(a+j)/i]+dp[i+1][j])%mod;
    int ans=0;
    for(int i=0;i<=sum;++i)
        ans=(ans+1ll*dp[1][i]*i)%mod;
    printf("%d",(1ll*k*qpow(k+1,n)%mod*n%mod*(mod+1)/2%mod-ans+mod)%mod);
}

Problem H.

这题,崩坏的条件是上下和左右都要满足至少一格崩坏或不存在。

考虑每个子矩形,他与整块大陆脱离的最小代价设为dp(x1,y1,x2,y2),那么枚举他作为哪一个更大的矩形的一角(4个方向都枚举),脱离的代价就是如下区域的冰山数:

***### ###***
***### ###***
***### ###***
###111 111###
###111 111###
###111 111###
(***是正在考虑的矩形,###是要计算的区域,111是无关紧要的区域)

那么答案就是下图#位置的冰山数+dp值(4个方向都枚举):

###***
###***
##P***
******
******
******

复杂度O(n6)(???

#include<bits/stdc++.h>
using namespace std;
int dp[50][50][50][50],a[50][50],n,m,px,py,ret=0x3f3f3f3f;
char s[50][50];
int g(int x1,int y1,int x2,int y2){
    x1--,x2--,y1--,y2--;
    return a[x2][y2]-a[x2][y1]-a[x1][y2]+a[x1][y1];
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i)scanf("%s",s[i]+1);
    for(int i=1;i<=n;++i)
        for(int j=1;j<=m;++j){
            a[i][j]=a[i-1][j]+a[i][j-1]-a[i-1][j-1]+(s[i][j]=='#');
            if(s[i][j]=='P')px=i,py=j;
        }
    memset(dp,0x3f,sizeof(dp)),dp[1][1][n+1][m+1]=0;
    for(int w=n;w>=1;--w)
        for(int h=m;h>=1;--h)
            for(int i=1;i+w<=n+1;i++)if(i<=px&&px<i+w)
                for(int j=1;j+h<=m+1;j++)if(j<=py&&py<j+h){
                    int& ans=dp[i][j][i+w][j+h];
                    for(int _w=w;_w<=n+1;++_w)
                        for(int _h=h;_h<=m+1;++_h)if(_w!=w||_h!=h){
                            ans=min(ans,dp[i][j][i+_w][j+_h]+g(i+w,j,i+_w,j+h)+g(i,j+h,i+w,j+_h));
                            if(j+h-_h>=1)ans=min(ans,dp[i][j+h-_h][i+_w][j+h]+g(i,j+h-_h,i+w,j)+g(i+w,j,i+_w,j+h));
                            if(i+w-_w>=1)ans=min(ans,dp[i+w-_w][j][i+w][j+_h]+g(i+w-_w,j,i,j+h)+g(i,j+h,i+w,j+_h));
                            if(j+h-_h>=1&&i+w-_w>=1)ans=min(ans,dp[i+w-_w][j+h-_h][i+w][j+h]+g(i,j+h-_h,i+w,j)+g(i+w-_w,j,i,j+h));
                        }
                    if(i<=px&&px<i+w&&j<=py&&py<j+h){
                        ret=min(ret,ans+g(i,j,px+1,py+1));
                        ret=min(ret,ans+g(px,py,i+w,j+h));
                        ret=min(ret,ans+g(i,py,px+1,j+h));
                        ret=min(ret,ans+g(px,j,i+w,py+1));
                    }
                }
    printf("%d",ret);
}

Problem I.
题解证明了一个结论:设最后排名为i的队为ai
那么ai必定会与ai|2j满足且ai<ai|2j

那么ii|2j连一条边。这样就形成了一个DAG。可以通过被确定的点求出每个点能取的区间[L,R]
枚举[0,2n1],到一个区间就把他加入堆。每次取出R最小的将i分配给他。无解就顺便判了。

这样得到了一个合法的排名。可以将二进制位反转得到原序列。大概就跟FFT那个位运算变换一样。

0 1 2 3 4 5 6 7->0 4 2 6 1 5 3 7
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> par;
int a[1<<18],n,m,bh[1<<18];
par b[1<<18];
vector<par>v[1<<18];
priority_queue<par,vector<par>,greater<par> >pq;
int main(){
    scanf("%d",&n),m=1<<n;
    for(int i=0;i<m;++i){
        bh[i]=(bh[i>>1]>>1)|((i&1)<<(n-1));
        scanf("%d",&a[i]),a[i]--;
        if(a[i]>=0)b[i]=par(a[i],a[i]);
        else b[i]=par(0,m-1);
    }
    for(int i=m-1;i>=0;--i)
        for(int j=0;j<n;++j)if(~i>>j&1)
            b[i].second=min(b[i].second,b[i^(1<<j)].second);
    for(int i=0;i<m;++i)
        for(int j=0;j<n;++j)if(~i>>j&1)
            b[i^(1<<j)].first=max(b[i^(1<<j)].first,b[i].first);
    for(int i=0;i<m;++i)
        if(b[i].first>b[i].second)return puts("NO"),0;
        else v[b[i].first].push_back(par(b[i].second,i));
    for(int i=0;i<m;++i){
        for(int j=0;j<v[i].size();++j)
            pq.push(v[i][j]);
        if(pq.empty())continue;
        if(pq.top().first<i)return puts("NO"),0;
        else a[pq.top().second]=i,pq.pop();
    }
    puts("YES");
    for(int i=0;i<m;++i)
        printf("%d ",a[bh[i]]+1);
}

Problem J.

这题。。。可以看题解,题解有一个奇妙的算法

其实可以点分治,每次只考虑经过某个点的边。这样每个子树内的点只连其他子树中最小的vali+depi

只有O(nlogn)条边,时间复杂度O(nlog2n)

#include<bits/stdc++.h>
#define maxn 200100
using namespace std;
typedef long long ll;
typedef pair<ll,int> par;
struct edge{
    int r,nxt,w;
}e[maxn<<1];
struct data{
    int u,v;
    ll w;
    int operator<(const data& d)const{return w<d.w;}
}d[maxn*100];
int f[maxn],head[maxn],tp,esz,num,mn,rt,vis[maxn],sz[maxn],A[maxn],a[maxn],n;
void addedge(int u,int v,int w){
    e[++esz].r=v;e[esz].nxt=head[u];head[u]=esz;e[esz].w=w;
    e[++esz].r=u;e[esz].nxt=head[v];head[v]=esz;e[esz].w=w;
}
void pre(int u,int f){
    num++;
    for(int t=head[u];t;t=e[t].nxt)if(!vis[e[t].r]&&e[t].r!=f)
        pre(e[t].r,u);
}
int findrt(int u,int f){
    int sz=1,s=0,mxsize=0;
    for(int t=head[u];t;t=e[t].nxt)if(!vis[e[t].r]&&e[t].r!=f){
        s=findrt(e[t].r,u),sz+=s;
        if(mxsize<s)mxsize=s;
    }
    if(mxsize<num-sz)mxsize=num-sz;
    if(mxsize<mn)mn=mxsize,rt=u;
    return sz;
}
par cal(int u,int f,ll d){
    par ret=par(a[u]+d,u);
    for(int t=head[u];t;t=e[t].nxt)if(!vis[e[t].r]&&e[t].r!=f)
        ret=min(ret,cal(e[t].r,u,d+e[t].w));
    return ret;
}
void join(int u,int f,ll d,par p){
    ::d[++tp]=data{u,p.second,p.first+a[u]+d};
    for(int t=head[u];t;t=e[t].nxt)if(!vis[e[t].r]&&e[t].r!=f)
        join(e[t].r,u,d+e[t].w,p);
}
void sol(int u){
    num=0,mn=1<<30,rt=u;
    pre(u,0),findrt(u,0);
    vis[u=rt]=1;
    int ptr=0;
    par mn(a[u],u);
    for(int t=head[u];t;t=e[t].nxt)if(!vis[e[t].r])A[++ptr]=t;
    for(int i=1,t;i<=ptr;++i){
        t=A[i];
        join(e[t].r,u,e[t].w,mn);
        mn=min(mn,cal(e[t].r,u,e[t].w));
    }
    mn=par(a[u],u);
    for(int i=ptr,t;i>=1;--i){
        t=A[i];
        join(e[t].r,u,e[t].w,mn);
        mn=min(mn,cal(e[t].r,u,e[t].w));
    }
    ::d[++tp]=data{u,mn.second,mn.first+a[u]};
    for(int t=head[u];t;t=e[t].nxt)if(!vis[e[t].r])sol(e[t].r);
}
int find(int x){
    return x==f[x]?x:f[x]=find(f[x]);
}
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;++i)scanf("%d",&a[i]);
    for(int i=2,u,v,w;i<=n;++i){
        scanf("%d%d%d",&u,&v,&w);
        addedge(u,v,w);
    }
    sol(1);
    sort(d+1,d+tp+1);
    long long sum=0;
    for(int i=1;i<=n;++i)f[i]=i;
    for(int i=1;i<=tp;++i){
        int u=find(d[i].u),v=find(d[i].v);
        if(u!=v)sum+=d[i].w,f[u]=v;
    }
    printf("%lld",sum);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值