CSP2019题解
T1 格雷码
认证前几天某odgd还说不考格雷码
然后就考了
考虑从高到低处理每一位,设当前处理到了第\(i\)位,待处理数字是\(j\)的状态为\(f(i,j)\)。
-
若\(j<2^{i-1}\),则输出
0
,处理\(f(i-1,j)\)。 -
若\(j\geqslant 2^{i-1}\),则输出
1
,将\(j\)的后\(i\)位取反,处理\(f(i-1,j)\)。
另外,要注意1ull<<64
爆unsigned long long
的问题。
code:
#include<stdio.h>
int n;
char c=0;
unsigned long long val=0;
void dfs(int p,unsigned long long q){
if(q<1ull<<p-1){
printf("0");
}else{
printf("1");
q-=1ull<<p-1;
q=(1ull<<p-1)-q-1;
}if(p-1>0)dfs(p-1,q);
}
int main(){
scanf("%d",&n);
while(c<'!')c=getchar();
while(c>='!')val=val*10+(c-'0'),c=getchar();
dfs(n,val);
}
T2 括号树
一道树上dp的好题不会栈的做法啊啊啊
设\(t[i],s[i]\):
-
若第\(i\)个括号为\()\),且在根结点到\(i\)号结点的简单路径上,存在节点\(j\),使得从\(j\)到\(i\)按结点经过顺序依次排列组成的字符串为合法括号串,则\(t[i]\)为满足条件的\(j\)中深度最小的点,\(s[i]\)为满足条件的\(j\)的数量。
-
若第\(i\)个括号为\()\),且在根结点到\(i\)号结点的简单路径上,不存在节点\(j\),使得从\(j\)到\(i\)按结点经过顺序依次排列组成的字符串为合法括号串,则\(t[i]=s[i]=0\)。
-
若第\(i\)个括号为\((\),则\(t[i]=i,s[i]=0\)。
转移:设当前讨论到了\(p\)号节点,需要讨论它的所有儿子
更新信息:若第\(i\)个括号为\()\),则\(t[p]\)需要一直往上跳到\(q\),使得\(q\)为满足从\(q\)到\(p\)按结点经过顺序依次排列组成的字符串为合法括号串的\(q\)中深度最小的点,否则\(t[p]=p\)。
- 当儿子为\((\),则令\(t[son]=son,s[son]=0\)。
- 当儿子为\()\):
- 当该节点为\((\),则\(t[son]=f[p]\),\(s[son]=s[f[p]]+1\)
- 当该节点为\()\):
- 当\(t[p]==1\)或\(t[p]==0\),说明没有与儿子配对的\((\),则\(t[son]=s[son]=0\)。
- 否则说明有与儿子配对的\((\),则\(t[son]=f[p]\),\(s[son]=s[f[f[p]]]+1\)。
code:
#include<cstdio>
int Last[500002],Next[500002],val[500002],f[500002];
int t[500002],g[500002],n,tp=0;
long long s[500002],ans=0;
char c;
void dfs(int p){
if(!val[p])t[p]=p;
else{
if(!s[p])t[p]=0;
else{
if(val[f[t[p]]])t[p]=t[f[t[p]]];
}
}
for(int i=Last[p];i;i=Next[i]){
if(val[i]){
if(!val[p])t[i]=p,s[i]=s[f[p]]+1;
else{
if(t[p]==0||t[p]==1)s[i]=0,t[i]=0;
else t[i]=f[t[p]],s[i]=s[f[t[i]]]+1;
}
}dfs(i);
}
}
void dfs1(int p){
ans^=s[p]*p;
for(int i=Last[p];i;i=Next[i]){
s[i]+=s[p];
dfs1(i);
}
}
int main(){
scanf("%d",&n);
c=getchar();
while(c<'!')c=getchar();
while(c>='!')val[++tp]=c==')',c=getchar();
for(int i=2;i<=n;i++){
scanf("%d",&f[i]);
Next[i]=Last[f[i]];Last[f[i]]=i;
}dfs(1);dfs1(1);printf("%lld",ans);
}
T3 树上的数
就是乱搞贪*害我投了150分钟进去。
思考一下每一个数字在树上移动的轨迹,设\(i\)移动轨迹为\(e_0,p_{i,0},e_{i,1},p_{i,2},e_{i,2},\dots,p_{i,n_i-1},e_{i,n_i-1},p_{i,n_i},e_0\)(前后补上\(e_0\)),容易发现以下几条规律:
- 对于每个节点\(u\),它在这些轨迹中出现次数为其度数\(+1\)。
- 对于每一条边\(e_i(u,v)\),它在这些轨迹中出现次数为2,且一次为\(u,e_i,v\),一次为\(v,e_i,u\)。
- 对于每个节点\(u\),不存在任意一个数字集合\(S\),使\(S\)中每一个数字的移动轨迹中与\(u\)相邻的边都出现过两次,且不同边的数量不为\(u\)的度数\(+1\)。(感性理解)
然后就有一个很暴力的思路:对每一个点\(u\)维护一个并查集,记录哪些边在已讨论过的路径中出现过\(e_i,u,e_j\)的情况,然后贪心,从\(1\text{~}n\)枚举每一个节点结束时可能的最小数字。
\(O(Tn^2\log n)\),相信\(\text{CCF}\)少爷机。
code:
#include<cstdio>
int Last[2002],Next[4002],End[4002],Len[4002],val[2002],mk[2002],rc[2002],T,n,f[2002],mp[2002][2002];
int ff[2002][2002],cntt[2002],ts[2002][2002],tb[2002][2002],ans[2002];
inline int gf(int p,int q){return ff[p][q]==q?q:(ff[p][q]=gf(p,ff[p][q]));}
void dfs(int p){
int F=f[p],r=gf(p,F),k=gf(p,0);
if(r!=k||ts[p][k]==cntt[p]||ts[p][k]>tb[p][k]+1)rc[p]=1;
for(int i=Last[p];i;i=Next[i])if(End[i]!=f[p]&&!Len[i]){
int s=gf(p,End[i]);f[End[i]]=p;
if(s!=r||ts[p][s]==cntt[p]||ts[p][s]>tb[p][s]+1)dfs(End[i]);
}
}
int main(){
scanf("%d",&T);
while(T--){
scanf("%d",&n);
for(int i=1;i<=n;i++){
Last[i]=mk[i]=ans[i]=0;
scanf("%d",&val[i]);cntt[i]=1;
}
for(int i=1;i<=n;i++){
for(int j=0;j<=n;j++){
ff[i][j]=j;
ts[i][j]=1;
tb[i][j]=0;
mp[i][j]=0;
}
}
for(int i=2;i<=n+n-2;i+=2){
scanf("%d%d",&End[i+1],&End[i]);Len[i]=Len[i+1]=0;
mp[End[i+1]][End[i]]=i;
mp[End[i]][End[i+1]]=i+1;
cntt[End[i]]++;cntt[End[i+1]]++;
Next[i]=Last[End[i+1]];Last[End[i+1]]=i;
Next[i+1]=Last[End[i]];Last[End[i]]=i+1;
}for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++)rc[j]=0,f[j]=0;
dfs(val[i]);
for(int j=1;j<=n;j++){
if(!mk[j]&&rc[j]&&val[i]!=j){
ans[i]=j;mk[j]=1;
int p=j,tmp=0,tmp2=0;
while(1){
if(tmp){
int s=gf(tmp,p),t=gf(tmp,tmp2);
ff[tmp][s]=t;
ts[tmp][t]+=ts[tmp][s];
tb[tmp][t]+=tb[tmp][s]+1;
}
Len[mp[f[p]][p]]=1;tmp2=tmp;tmp=p;p=f[p];
if(!tmp)break;
}break;
}
}
}for(int i=1;i<=n;i++)printf("%d ",ans[i]);puts("");
}
}
T4 Emiya 家今天的饭
看到Yazid就想起这到题,还都是dp
首先,很容易就能想到用合法的方案数减去非法的方案数。
设\(f[t][i][j][k]\)表示当前讨论到第\(t\)种物品,第\(i\)种方法,一共用了\(j\)种方法,其中有\(k\)种方法用的是物品\(t\)。
然后可以滚掉\(t\)这一维,得到\(f[i][j][k]\),\(O(n^3m)\)。
还可以合并\(j,k\)两维,设\(f[i][j]\)表示当前讨论到第\(i\)种方法,用的方法数\(-\)用的物品\(t\)数\(\times 2=j\)。
\(f[i][j]=f[i-1][j]+f[i-1][j-1]*(sum[i]-a[i][t])+f[i-1][j+1]*a[i][t]\)
code:
#include<cstdio>
#include<memory.h>
#define inf 998244353
int a[102][2002],n,m,ans=1;
int dp[102][222],s[102];
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
s[i]=0;
for(int j=1;j<=m;j++){
scanf("%d",&a[i][j]);
s[i]+=a[i][j];
if(s[i]>=inf)s[i]-=inf;
}ans=1ll*ans*(s[i]+1)%inf;
}ans+=inf-1;
if(ans>=inf)ans-=inf;
for(int t=1;t<=m;t++){
memset(dp,0,sizeof(dp));
dp[0][n+1]=1;//n+1+a-b*2
for(int i=1;i<=n;i++){
for(int j=n+1-i;j<=n+1+i;j++){
dp[i][j]=(1ll*dp[i-1][j-1]*(s[i]+inf-a[i][t])+1ll*dp[i-1][j+1]*a[i][t]+dp[i-1][j])%inf;
}
}for(int i=n;i>=1;i--){
ans+=inf-dp[n][i];
while(ans>=inf)ans-=inf;
}
}printf("%d ",ans);
}
T5 划分
盲猜结论题
先前缀和一遍,再设\(t[i]\)为前\(i\)个数的最优划分中倒数第二段的最后一个位置。
维护一个单调栈,保证里面的元素\(p\)满足\(s[p]\times 2-s[t[p]]\)单调不降,再在上面维护一个指针,指针只往右移。
每次找到栈里最大的满足\(s[i]>=s[p]\times 2-s[t[p]]\)的数,即为\(t[i]\)。(我也很迷)
再插入\(i\),总时间\(O(n)\)。
吐槽高精慢死了
code:
#include<cstdio>
#include<deque>
#include<bits/stdc++.h>
__int128 ans=0,e=1;
int t[40000002],n,T;
long long s[40000002];
int S[40000002],g[40000002],tp=0,pos=0;
int main(){
scanf("%d%d",&n,&T);
if(!T)for(int i=1;i<=n;i++)scanf("%lld",&s[i]),s[i]+=s[i-1];
else{
long long x,y,z,m,lj=0;
scanf("%lld%lld%lld%lld%lld%lld",&x,&y,&z,&s[1],&s[2],&m);
for(int i=3;i<=n;i++)s[i]=(x*s[i-1]+y*s[i-2]+z)&1073741823ll;
while(m--){
scanf("%lld%lld%lld",&x,&y,&z);
for(int i=lj+1;i<=x;i++)s[i]=s[i]%(z-y+1)+y;
lj=x;
}for(int i=1;i<=n;i++)s[i]+=s[i-1];
}S[++tp]=0;
for(int i=1;i<=n;i++){
while(pos<tp&&s[i]-s[S[pos+1]]>=s[S[pos+1]]-s[t[S[pos+1]]])pos++;
t[i]=S[pos];g[i]=g[t[i]]+1;
while(tp&&s[S[tp]]+s[S[tp]]-s[t[S[tp]]]>=s[i]+s[i]-s[t[i]])tp--;
S[++tp]=i;
}for(int i=n;i;i=t[i]){
ans+=e*(s[i]-s[t[i]])*(s[i]-s[t[i]]);
}if(ans<1e18){
long long k=ans;
printf("%lld",k);
}else{
long long k1=ans/1000000000000000000,k2=ans%1000000000000000000;
printf("%lld%18lld",k1,k2);
}
}
T6 树的重心
CCF非专业级植树能力认证
先以1号点为根把树提起来,然后对每个节点,统计它对答案的贡献。
设\(i\)的最大儿子为\(s[i][0]\),次大为\(s[i][1]\),以\(i\)为根的子树大小为\(siz[i]\)。
设切掉的边两端更低的一个点为\(u\),当前讨论到节点\(p\)。
若\(u\)为\(p\)的祖先(含\(p\)),则\(p\)为重心当且仅当\(2\times siz[s[p][0]]\leqslant s[u]\leqslant 2\times s[p]\)
若\(u\)在\(p\)的最大儿子的子树中,则\(p\)为重心当且仅当\(\max(siz[s[p][1]],siz[s[p][0]]-siz[u],siz[1]-siz[p])\times 2\leqslant siz[1]-siz[u]\)
若\(u\)在\(p\)的其它儿子的子树中,则\(p\)为重心当且仅当\(\max(siz[s[p][0]],siz[1]-siz[p])\times 2\leqslant siz[1]-siz[u]\)
否则,\(p\)为重心当且仅当\(max(siz[s[p][0]],siz[1]-siz[p]-siz[u])\times 2\leqslant siz[1]-siz[u]\) *这种情况可以用非子树减去祖先来求
化简一下,用主席树维护子树,用倍增维护一下祖先。
code:
#include<cstdio>
int Last[300002],Next[600002],End[600002],f[300002][22];
int t[300002],h[300002],s[300002],ss[300002][2],tot,T,n;
long long d[300002],ans;
inline int Max(int a,int b){return a>b?a:b;}
struct node{
node *ls,*rs;
int cnt;
}tr[8000002],*null=tr,*top,*rt[300002];
void init(){
null->ls=null->rs=null;
null->cnt=0;rt[0]=null;
}
node *addnode(){
node *p=++top;
p->ls=p->rs=null;
p->cnt=0;
return p;
}
void add(int p,int l,int r,node *t,node *lt){
t->ls=lt->ls;t->rs=lt->rs;t->cnt=lt->cnt+1;
if(l==r)return;
if(l+r>>1>=p)add(p,l,l+r>>1,t->ls=addnode(),lt->ls);
else add(p,l+r+2>>1,r,t->rs=addnode(),lt->rs);
}
int get(int L,int R,int l,int r,node *lt,node *t){
if(L<=l&&r<=R)return t->cnt-lt->cnt;
if(L>R||lt==t)return 0;
if(L<1)L=1;if(R>n)R=n;
long long ans=0;
if(l+r>>1>=L)ans+=get(L,R,l,l+r>>1,lt->ls,t->ls);
if(l+r>>1<R)ans+=get(L,R,l+r+2>>1,r,lt->rs,t->rs);
return ans;
}
void dfs1(int p,int F){
d[p]=d[F]+1;
f[p][0]=F;
s[p]=1;h[p]=0;
ss[p][0]=ss[p][1]=0;
for(int i=1;i<=19;i++)f[p][i]=f[f[p][i-1]][i-1];
for(int i=Last[p];i;i=Next[i])if(End[i]!=F){
dfs1(End[i],p);s[p]+=s[End[i]];
if(s[End[i]]>s[h[p]])h[p]=End[i];
if(s[End[i]]>ss[p][1])ss[p][1]=s[End[i]];
if(ss[p][1]>ss[p][0])ss[p][1]^=ss[p][0]^=ss[p][1]^=ss[p][0];
}
}
void dfs2(int p){
t[p]=++tot;
add(s[p],1,n,rt[t[p]]=addnode(),rt[t[p]-1]);
if(h[p])dfs2(h[p]);
for(int i=Last[p];i;i=Next[i])if(End[i]!=f[p][0]&&End[i]!=h[p])dfs2(End[i]);
if(p!=1){
int l=p,r=p,lb=2*s[p],rb=2*ss[p][0];
for(int i=19;i>=0;i--){
if(f[l][i]&&s[f[l][i]]<=lb)l=f[l][i];
if(f[r][i]&&s[f[r][i]]<rb)r=f[r][i];
}if(s[r]<rb&&r!=1)r=f[r][0];
if(s[l]<=lb&&l!=1)l=f[l][0];
ans+=(d[r]-d[l])*p;
}
}
void dfs3(int p){
for(int i=Last[p];i;i=Next[i])if(End[i]!=f[p][0])dfs3(End[i]);
if(h[p]){
ans+=1ll*p*get(1,s[1]-2*Max(ss[p][0],s[1]-s[p]),1,n,rt[t[h[p]]+s[h[p]]-1],rt[t[p]+s[p]-1]);
ans+=1ll*p*get(2*ss[p][0]-s[1],s[1]-2*Max(ss[p][1],s[1]-s[p]),1,n,rt[t[p]],rt[t[h[p]]+s[h[p]]-1]);
}if(p!=1){
ans+=1ll*p*get(s[1]-2*s[p],s[1]-2*ss[p][0],1,n,rt[0],rt[t[p]-1]);
ans+=1ll*p*get(s[1]-2*s[p],s[1]-2*ss[p][0],1,n,rt[t[p]+s[p]-1],rt[n]);
int l=f[p][0],r=f[p][0],lb=s[1]-2*ss[p][0],rb=s[1]-2*s[p];
for(int i=19;i>=0;i--){
if(f[l][i]&&s[f[l][i]]<=lb)l=f[l][i];
if(f[r][i]&&s[f[r][i]]<rb)r=f[r][i];
}if(s[r]<rb)r=f[r][0];
if(s[l]<=lb)l=f[l][0];
ans-=(d[r]-d[l])*p;
}
}
int main(){
init();
scanf("%d",&T);
while(T--){
scanf("%d",&n);top=tr;d[0]=0;ans=0;tot=0;
for(int i=1;i<=n;i++)Last[i]=0;
for(int i=1;i<n+n-2;i+=2){
scanf("%d%d",&End[i+1],&End[i]);
Next[i]=Last[End[i+1]];Last[End[i+1]]=i;
Next[i+1]=Last[End[i]];Last[End[i]]=i+1;
}dfs1(1,0);dfs2(1);dfs3(1);printf("%lld\n",ans);
}
}
题出的好!难度适中,覆盖知识点广,题目又着切合实际的背景,解法比较自然。给出题人点赞 !