5.28 考试修改+总结
今天又是一个悲伤的故事,所有排名比我高的人第一题都A了
而我第一题爆零了
但是开心的事情是:第一题没有说是简单图,所以题解是错的
不管怎么样,将错就错吧
今天下午断网了,所以这时候才写blog
第一题
由于题目中没有给出欧拉图的概念,所以我完全不知道它在说啥,于是就爆零了
然后欧拉图就是存在欧拉回路的简单图,具有以下特点:
1、联通
2、度数都是偶数
显然我们将错就错看题目的话,把欧拉图数目*(n*(n-1)/2+1)就可以得到答案了
然后我们很容易知道度数均为偶数的图的数目是2^((n-1)*(n-2)/2)
就是考虑提出其中的一个点,剩余的点任意构图,然后对于提出的点来说有且仅有一种方案使现在的图度数均为偶数
之后DP利用容斥原理可以求出联通的度数均为偶数的数目,做法是用总的-不连通的
算不连通的只需要枚举1所在的联通块大小就可以了
时间复杂度O(n^2)
#include<cstdio> #include<cstdlib> #include<cstring> #include<iostream> #include<algorithm> using namespace std; typedef long long LL; const int maxn=2010; const int mod=1e9+7; int n; LL h[maxn],g[maxn],f[maxn]; LL jc[maxn],inv[maxn]; LL pow_mod(LL v,int p){ LL tmp=1; while(p){ if(p&1)tmp=tmp*v%mod; v=v*v%mod;p>>=1; }return tmp; } LL C(int n,int m){return jc[n]*inv[m]%mod*inv[n-m]%mod;} int main(){ scanf("%d",&n); jc[0]=1; for(int i=1;i<=n;++i)jc[i]=jc[i-1]*i%mod; inv[n]=pow_mod(jc[n],mod-2); for(int i=n-1;i>=0;--i)inv[i]=inv[i+1]*(i+1)%mod; h[1]=1;f[1]=1;g[1]=0; for(int i=2;i<=n;++i)h[i]=pow_mod(2LL,(i-1)*(i-2)/2); for(int i=2;i<=n;++i){ f[i]=h[i]; for(int j=1;j<=i-1;++j){ f[i]=f[i]-C(i-1,j-1)*f[j]%mod*h[i-j]%mod; if(f[i]<0)f[i]+=mod; } }f[n]=f[n]+n*(n-1)*pow_mod(2LL,mod-2)%mod*f[n]%mod; if(f[n]>=mod)f[n]-=mod; printf("%lld\n",f[n]); return 0; }
然后我们很容易发现其实算不连通的时候的那个式子是个卷积形式
于是我们就可以FFT,我闲的无聊写了一发CDQ+FFT
由于模数不是很支持,所以我们每次要做9次DFT
然后虽然理论复杂度O(nlog^2n),但是只能跑过20000的数据QAQ
#include<cstdio> #include<cstring> #include<iostream> #include<cstdlib> #include<algorithm> #include<cmath> #define G 3 using namespace std; const int maxn=400010; typedef long long LL; int n,N,len; const int mod=1e9+7; const int M=30000; const long double pi=acos(-1.0); int jc[maxn],inv[maxn]; int h[maxn],f[maxn]; int rev[maxn],Num[22]; int a[maxn],b[maxn],c[maxn]; int a0[maxn],b0[maxn],a1[maxn],b1[maxn]; struct cpx{ long double r,i; cpx(long double r=0,long double i=0):r(r),i(i){} }A[maxn],B[maxn],C[maxn]; cpx operator +(const cpx &A,const cpx &B){return cpx(A.r+B.r,A.i+B.i);} cpx operator -(const cpx &A,const cpx &B){return cpx(A.r-B.r,A.i-B.i);} cpx operator *(const cpx &A,const cpx &B){return cpx(A.r*B.r-A.i*B.i,A.r*B.i+A.i*B.r);} void FFT(cpx *A,int n,int type){ for(int i=0;i<n;++i)if(i<rev[i])swap(A[i],A[rev[i]]); for(int k=0;(1<<k)<n;++k){ int m=(1<<k),m2=(m<<1); long double o=2*pi/m2*type; cpx wn(cos(o),sin(o)); for(int i=0;i<n;i+=m2){ cpx w(1,0); for(int j=0;j<m;++j){ cpx x=A[i+j],y=A[i+j+m]*w; A[i+j]=x+y;A[i+j+m]=x-y; w=w*wn; } } } if(type==-1)for(int i=0;i<n;++i)A[i].r/=n; } void mul(int *a,int *b,int *c){ for(int i=0;i<N;++i)A[i]=cpx(a[i],0),B[i]=cpx(b[i],0); FFT(A,N,1);FFT(B,N,1); for(int i=0;i<N;++i)A[i]=A[i]*B[i]; FFT(A,N,-1); for(int i=0;i<N;++i)c[i]=((LL)(A[i].r+0.5))%mod; } void mul_mod(int *a,int *b,int *c){ for(int i=0;i<N;++i)a0[i]=a[i]/M,b0[i]=b[i]/M; mul(a0,b0,a0); for(int i=0;i<N;++i){ c[i]=1LL*a0[i]*M%mod*M%mod; a1[i]=a[i]%M;b1[i]=b[i]%M; } mul(a1,b1,a1); for(int i=0;i<N;++i){ c[i]=c[i]+a1[i]; if(c[i]>=mod)c[i]-=mod; a1[i]=a1[i]+a0[i]; if(a1[i]>=mod)a1[i]-=mod; a0[i]=a[i]/M+a[i]%M; b0[i]=b[i]/M+b[i]%M; } mul(a0,b0,a0); for(int i=0;i<N;++i){ c[i]=c[i]+1LL*M*(a0[i]-a1[i]+mod)%mod; if(c[i]>=mod)c[i]-=mod; }return; } LL pow_mod(LL v,int p){ LL tmp=1; while(p){ if(p&1)tmp=tmp*v%mod; v=v*v%mod;p>>=1; }return tmp; } void Solve(int L,int R){ if(L==R)return; int mid=(L+R)>>1; Solve(L,mid); for(N=1,len=0;N<(R-L+1);N<<=1,len++); for(int i=0;i<N;++i)rev[i]=rev[i>>1]>>1|((i&1)<<(len-1)); for(int i=0;i<N;++i)a[i]=b[i]=0; for(int i=L;i<=mid;++i)a[i-L]=1LL*f[i]*inv[i-1]%mod; for(int i=0;i<N;++i)b[i]=h[i]; mul_mod(a,b,c); for(int i=mid+1;i<=R;++i){ f[i]=f[i]-1LL*jc[i-1]*c[i-L]%mod; if(f[i]<0)f[i]+=mod; } Solve(mid+1,R); } int main(){ scanf("%d",&n); jc[0]=1; for(int i=1;i<=n;++i)jc[i]=1LL*jc[i-1]*i%mod; inv[n]=pow_mod(jc[n],mod-2); for(int i=n-1;i>=0;--i)inv[i]=1LL*inv[i+1]*(i+1)%mod; h[1]=1;f[1]=1; for(int i=2;i<=n;++i){ h[i]=pow_mod(2LL,(i-1)*(i-2)/2); f[i]=h[i]; h[i]=1LL*h[i]*inv[i]%mod; } Solve(1,n); f[n]=f[n]+1LL*n*(n-1)*pow_mod(2LL,mod-2)%mod*f[n]%mod; if(f[n]>=mod)f[n]-=mod; printf("%d\n",f[n]); return 0; }
第二题
今天第二题被卡了10分常数,不过也因此学到了一些新技巧,也算因祸得福
显然我们应该考虑分治,分治的做法有两种:
第一种是题解的做法,就是我们直接劈开两半,然后统计过中间的答案
对左右各维护一个指针,每次向最大值较小的地方扩展,这样就可以保证当某一侧作为最大值的时候,另一侧一定是极大扩展区间
然后在扫的过程中用一个数组存下前缀和或者后缀和就可以了
这个做法其实略微有些麻烦,因为无论是前缀最大值还是后缀最大值都是单调的
相应的极大区间的端点也是单调的,我们只需要利用单调性扫一遍左边再扫一遍右边就可以了
注意扫的时候因为最大值有可能同时在左边和右边出现,所以我们扫左边的时候规定右边严格小于,扫右边的时候规定左边小于等于就可以了
复杂度O(nlogn)
第二种做法是我们考虑找到当前区间的最大值,然后把它劈成两半分治
我们也只需要统计过中间的答案就可以了,这个时候的最大值已经固定了,答案是易于统计的
但是注意这里我们理论上是不能左右都扫一遍的,因为这里不是均分两半,左右都扫一遍极限复杂度O(n^2)
出题人后来说数据是随机的QAQ然后考场上有一片人就这样过去了(随机数据复杂度O(nlogn))
很容易发现其实我们不用写分治,用单调队列预处理每个点作为最大值的左右端点然后暴力扫就可以了
时间复杂度跟上面同阶,这个做法虽然因为数据随机能过,但是不是很优美,因为可以被卡住
我们会发现问题转化为枚举一侧统计另一侧有多少个数为x,显然我们可以用启发式思想,每次枚举较短的那一侧
不难发现每个点每一次被枚举,其所在区间长度至少/2,所以每个点对枚举的贡献是O(logn)
枚举的时间复杂度是O(nlogn),然后查询显然是可以用可持久化线段树完成的
总时间复杂度O(nlog^2n),由于这样分治可能会爆栈,所以我考场上写了BFS,结果被卡了10分常数
优化了一下主席树就A了,至于主席树的优化如果你发现当前区间的和是0,你就不用向下递归(这对于主席树是很大的优化,因为想想主席树的建树过程。。)
后来发现数据随机,DFS不会爆栈,改成DFS不优化主席树也能A,考场上想多了QAQ 忧桑ing
但是这比题解复杂度多个log,可以继续优化么?可以!
ymx提出来你会发现DFS的过程中我们不询问,而把询问拆成两个前缀询问离线做,询问最多有O(nlogn)个
不难发现由于DFS的性质,最后得到的询问序列天然有序,之后我们离线O(n)扫一遍就可以O(nlogn)的处理所有询问了
时间复杂度O(nlogn),实测比题解不知道快到哪里去了
贴一下上午的代码,作为主席树的优化留念吧:
#include<cstdio> #include<iostream> #include<cstdlib> #include<algorithm> #include<cstring> #include<queue> using namespace std; typedef long long LL; const int maxn=300010; int n,k,mod; int a[maxn]; int vis[1000010],tot=0; int mx[maxn<<2],pos[maxn<<2]; int sum[maxn]; int cnt=0,rt[maxn]; LL ans=0; struct Seg_Tree{ int L,R,v; }t[8000010]; struct OP{ int L,R; OP(int L=0,int R=0):L(L),R(R){} }; queue<OP>Q; void read(int &num){ num=0;char ch=getchar(); while(ch<'!')ch=getchar(); while(ch>='0'&&ch<='9')num=(num<<1)+(num<<3)+ch-'0',ch=getchar(); } void build_tree(int o,int L,int R){ if(L==R){mx[o]=a[L];pos[o]=L;return;} int mid=(L+R)>>1; build_tree(o<<1,L,mid); build_tree(o<<1|1,mid+1,R); if(mx[o<<1]>mx[o<<1|1])mx[o]=mx[o<<1],pos[o]=pos[o<<1]; else mx[o]=mx[o<<1|1],pos[o]=pos[o<<1|1]; } pair<int,int>ask_mx(int o,int L,int R,int x,int y){ if(L>=x&&R<=y)return make_pair(mx[o],pos[o]); int mid=(L+R)>>1; if(y<=mid)return ask_mx(o<<1,L,mid,x,y); else if(x>mid)return ask_mx(o<<1|1,mid+1,R,x,y); else{ pair<int,int>A=ask_mx(o<<1,L,mid,x,y); pair<int,int>B=ask_mx(o<<1|1,mid+1,R,x,y); if(A.first>B.first)return A; return B; } } void build(int &o,int L,int R){ o=++cnt; if(L==R)return; int mid=(L+R)>>1; build(t[o].L,L,mid); build(t[o].R,mid+1,R); } void UPD(int &o,int L,int R,int p){ t[++cnt]=t[o];o=cnt; if(L==R){t[o].v++;return;} int mid=(L+R)>>1; if(p<=mid)UPD(t[o].L,L,mid,p); else UPD(t[o].R,mid+1,R,p); t[o].v=t[t[o].L].v+t[t[o].R].v; } int ask(int A,int B,int L,int R,int p){ if(A==B)return 0; if(L==R)return t[A].v-t[B].v; int mid=(L+R)>>1; if(p<=mid)return ask(t[A].L,t[B].L,L,mid,p); else return ask(t[A].R,t[B].R,mid+1,R,p); } void BFS(){ Q.push(OP(2,n)); while(!Q.empty()){ OP tmp=Q.front();Q.pop(); int L=tmp.L,R=tmp.R; int p=ask_mx(1,1,n,L,R).second; if(L<p-1)Q.push(OP(L,p-1)); if(p+1<R)Q.push(OP(p+1,R)); int Len=p-L,Ren=R-p; if(Ren<Len){ int tmp=sum[p]-a[p]+mod; if(tmp>=mod)tmp-=mod; if(vis[tmp])tmp=ask(rt[p-2],rt[L-2],1,tot,vis[tmp]); else tmp=0; ans+=tmp; for(int i=p+1;i<=R;++i){ tmp=sum[i]-a[p]+mod; if(tmp>=mod)tmp-=mod; if(!vis[tmp])continue; tmp=ask(rt[p-1],rt[L-2],1,tot,vis[tmp]); ans+=tmp; } }else{ int tmp=sum[p-1]+a[p]; if(tmp>=mod)tmp-=mod; if(vis[tmp])tmp=ask(rt[R],rt[p],1,tot,vis[tmp]); else tmp=0; ans+=tmp; for(int i=L-1;i<=p-2;++i){ tmp=sum[i]+a[p]; if(tmp>=mod)tmp-=mod; if(!vis[tmp])continue; tmp=ask(rt[R],rt[p-1],1,tot,vis[tmp]); ans+=tmp; } } }return; } int main(){ read(n);read(k);n++;mod=k; for(int i=2;i<=n;++i)read(a[i]); build_tree(1,1,n); vis[0]=++tot; for(int i=2;i<=n;++i){ a[i]%=k; sum[i]=sum[i-1]+a[i]; if(sum[i]>=k)sum[i]-=k; if(!vis[sum[i]])vis[sum[i]]=++tot; } build(rt[0],1,tot); for(int i=1;i<=n;++i){ rt[i]=rt[i-1]; UPD(rt[i],1,tot,vis[sum[i]]); } BFS(); printf("%lld\n",ans); return 0; }
第三题
我们考虑树是一条链的情况,我们用线段树维护最长连续子段和就可以了
那么对于一棵树,我们可以采用树链剖分转化成链的情况
但是注意到联通块可能是链和链的拼接,所以我们定义一个点点权是 他所有儿子所在的跟他不同的链的最大前缀和(负数为0)和他自己的点权
这样所有链的最长连续子段和的最大值就是答案了
我们把修改转成增量修改
每次修改最多修改logn条链,我们往上跳的时候维护影响到的点权就可以了
考场上大概不到半个小时就写完拍上了,然后测测发现可能会T
然后就又花了半个小时卡了卡常数,优化了一下建树过程,然后练习了一发树形DP。。
结果交上去至今还是rank1QAQ
值得一提的是维护所有链的最长连续字段和可以用堆,但是考场上的我用了另外一棵线段树QAQ
#include<cstdio> #include<cstring> #include<iostream> #include<cstdlib> #include<algorithm> using namespace std; const int maxn=100010; int n,m,u,v,type; int c[maxn]; int h[maxn],cnt=0; struct edge{ int to,next; }G[maxn<<1]; int dep[maxn],sz[maxn],son[maxn],fa[maxn]; int L[maxn],R[maxn],pos[maxn],fp[maxn],tot; int P[maxn],top[maxn],sum; int mx[maxn<<2]; int dp[maxn],tmp[maxn]; struct Seg_Tree{ int suf,pre,mx,sum; }t[maxn<<2]; void read(int &num){ int f=1;num=0;char ch=getchar(); while(ch<'!')ch=getchar(); if(ch=='-')f=-1,ch=getchar(); while(ch>='0'&&ch<='9')num=(num<<1)+(num<<3)+ch-'0',ch=getchar(); num*=f; } void add(int x,int y){ ++cnt;G[cnt].to=y;G[cnt].next=h[x];h[x]=cnt; } void DFS(int u,int f){ sz[u]=1;fa[u]=f; for(int i=h[u];i;i=G[i].next){ int v=G[i].to; if(v==f)continue; dep[v]=dep[u]+1; DFS(v,u); sz[u]+=sz[v]; if(sz[son[u]]<sz[v])son[u]=v; }return; } void Get_pos(int u,int f){ top[u]=f;pos[u]=++tot;fp[tot]=u; if(u==f){sum++;L[sum]=tot;} P[u]=sum; if(!son[u]){R[sum]=tot;return;} Get_pos(son[u],f); for(int i=h[u];i;i=G[i].next){ int v=G[i].to; if(v==fa[u]||v==son[u])continue; Get_pos(v,v); }return; } int Max(int a,int b){return a>b?a:b;} void UPD(int o,int L,int R,int p,int v){ if(L==R){ t[o].suf+=v;t[o].pre+=v; t[o].mx+=v;t[o].sum+=v; return; } int mid=(L+R)>>1; if(p<=mid)UPD(o<<1,L,mid,p,v); else UPD(o<<1|1,mid+1,R,p,v); int l=(o<<1),r=(l|1); t[o].sum=t[l].sum+t[r].sum; t[o].suf=Max(t[r].suf,t[r].sum+t[l].suf); t[o].pre=Max(t[l].pre,t[l].sum+t[r].pre); t[o].mx=Max(t[l].mx,t[r].mx); t[o].mx=Max(t[o].mx,t[l].suf+t[r].pre); } void build(int o,int L,int R){ if(L==R){ int u=fp[L]; t[o].suf=t[o].pre=t[o].mx=t[o].sum=dp[u]; return; } int mid=(L+R)>>1; build(o<<1,L,mid); build(o<<1|1,mid+1,R); int l=(o<<1),r=(l|1); t[o].sum=t[l].sum+t[r].sum; t[o].suf=Max(t[r].suf,t[r].sum+t[l].suf); t[o].pre=Max(t[l].pre,t[l].sum+t[r].pre); t[o].mx=Max(t[l].mx,t[r].mx); t[o].mx=Max(t[o].mx,t[l].suf+t[r].pre); } Seg_Tree ask(int o,int L,int R,int x,int y){ if(L>=x&&R<=y)return t[o]; int mid=(L+R)>>1; if(y<=mid)return ask(o<<1,L,mid,x,y); else if(x>mid)return ask(o<<1|1,mid+1,R,x,y); else{ Seg_Tree A=ask(o<<1,L,mid,x,y); Seg_Tree B=ask(o<<1|1,mid+1,R,x,y),C; C.sum=A.sum+B.sum; C.suf=Max(B.suf,B.sum+A.suf); C.pre=Max(A.pre,A.sum+B.pre); C.mx=Max(A.mx,B.mx); C.mx=Max(C.mx,A.suf+B.pre); return C; } } void modify(int o,int L,int R,int p,int v){ if(L==R){mx[o]=v;return;} int mid=(L+R)>>1; if(p<=mid)modify(o<<1,L,mid,p,v); else modify(o<<1|1,mid+1,R,p,v); mx[o]=Max(mx[o<<1],mx[o<<1|1]); } void Get_UPD(int u,int v){ while(u!=-1){ Seg_Tree A=ask(1,1,n,L[P[u]],R[P[u]]); UPD(1,1,n,pos[u],v); Seg_Tree B=ask(1,1,n,L[P[u]],R[P[u]]); modify(1,1,sum,P[u],B.mx); if(B.pre<0)B.pre=0; if(A.pre<0)A.pre=0; u=fa[top[u]];v=B.pre-A.pre; }return; } void Get_DP(int u,int f){ dp[u]=c[u];tmp[u]=c[u]; if(!son[u])return; for(int i=h[u];i;i=G[i].next){ int v=G[i].to; if(v==f||v==son[u])continue; Get_DP(v,u); if(tmp[v]>0)dp[u]+=tmp[v]; }Get_DP(son[u],u); if(tmp[son[u]]>0)tmp[u]=dp[u]+tmp[son[u]]; else tmp[u]=dp[u]; } int main(){ read(n);read(m); for(int i=1;i<=n;++i)read(c[i]); for(int i=1;i<n;++i){ read(u);read(v); add(u,v);add(v,u); }DFS(1,-1);Get_pos(1,1); Get_DP(1,-1);build(1,1,n); //for(int i=1;i<=n;++i)printf("%d %d\n",dp[i],tmp[i]); for(int i=1;i<=sum;++i){ Seg_Tree A=ask(1,1,n,L[i],R[i]); modify(1,1,sum,i,A.mx); } //for(int i=1;i<=n;++i)Get_UPD(i,c[i]); //for(int i=1;i<=n;++i)printf("%d\n",ask(1,1,n,pos[i],pos[i]).sum); while(m--){ read(type); if(type==2)printf("%d\n",max(mx[1],0)); else{ read(u);read(v); v=v-c[u];c[u]+=v; Get_UPD(u,v); } } return 0; }
今天考试学会了一些主席树的优化技巧
虽然考试被卡常数,而且第二题数据随机被一大波人水过去了,非常伤心
但是学到了新知识还是蛮高兴的QAQ
感觉自己的数据结构能力非常不错了,一般有思路的需要维护什么数据结构的题目都能很快的写出来
基本上不用改就能拍上(top_tree什么的除外)
题目略微有些坑,第一题的欧拉图造成了自己的知识障碍,然后读不懂题就只能爆零了
留下的题目什么的嘛:一些有关于图论方面的计数问题?并不知道能不能找到。。