QAQ 论考试犯蠢的窝 QAQ
今天考试四道题,窝居然自作主张的以为是四选三
然后就只搞了三道题
(但是后来发现剩下的那道题其实自己也是搞不出来的)
今天没有码农题,都是非常好的题目,但是因为自己太弱了
最后才搞了210收场,下午也是理解了好久才改完
第一题
考试的时候先想了一会容斥发现并不能做出来
于是弃疗写了O(n^2)的暴力,然后机智的窝发现暴力实际上是O(n*sqrt(n))的
因为当m*(m-1)/2>n的时候无解QAQ 然后就A掉了
设f(i,j)表示把j这个数分成i个互不相同的数的方案
考虑这i个数中有没有1,则:
1、没有1的情况,我们把所有数减1,方案数是等价的,是f(i,j-i)
2、有1的情况,我们考虑去掉这个1,那么剩下的数一定没有1,我们把剩余的数字全部减1,方案是等价的,是f(i-1,j-i)
考虑1的位置,可以得到这部分方案是f(i-1,j-i)*j
QAQ 其实这个做法跟 简单的多重背包 的>sqrt(n)时的做法是大同小异的 QAQ
#include<cstdio> #include<cstring> #include<algorithm> #include<iostream> #include<cstdlib> using namespace std; typedef long long LL; const int mod=998244353; const int maxn=100010; int n,m; int f[2][maxn]; int main(){ freopen("celebration.in","r",stdin); freopen("celebration.out","w",stdout); scanf("%d%d",&n,&m); if(1LL*(1+m)*m/2>n){printf("0 ");return 0;} f[0][0]=1; for(int j=1;j<=m;++j){ int now=(j&1),la=(now^1); int cnt=0; for(int i=0;i<j;++i)f[now][i]=0; for(int i=j;i<=n;++i){ f[now][i]=f[now][cnt]+1LL*f[la][cnt]*j%mod; if(f[now][i]>=mod)f[now][i]-=mod; cnt++; } }printf("%d ",f[m&1][n]); return 0; }
第二题
就是考试我弃掉的那道题目,由于solution太坑爹,几乎什么都没有说,理解将近一个半小时才弄懂了做法
首先我们考虑把x,y独立化,那么我们转换坐标令x=x+y且y=x-y,其实就是将坐标系翻转45度
之后我们的答案是sigma((x+y)^n*(x-y)^m)/(2^(n+m))
我们做一遍二项式展开,会得到(n+1)*(m+1)项
每一项是 sigma(C(n,i)*C(m,j)*(-1)^j*x^(n+m-i-j)*y^(i+j))
由于x,y都是独立的,且x和y的取值都有2^t种,所以问题转化为求sigma(x^i)
尝试着用矩阵描述每一次x的变换
不难发现每次x会变成(x+1)和(x-1)
那么考虑i次方得 (x+1)^i+(x-1)^i
然后再做一次二项式展开,我们不难得到答案是sigma(C(i,k)*2*x^k)(其中i和k的奇偶性相同)
发现当前的sigma(x^k)是已知的,且对i次的贡献系数为C(i,k)*2
这样我们就成功的用矩阵描述了每一次的变换,那么矩阵乘法+快速幂即可
之后代入式子还原出答案就好了
QAQ 然后本题略微有些卡常数,最后矩阵乘法用std的方式优化了一下才过最后一个点
优化方式是:用一个long long来求sigma(aik*bkj)
最后在赋值并取模就可以去掉二维数组的常数了(真是机智的做法,不知道为什么快了一倍
又学到了新姿势,开森!
#include<cstdio> #include<cstring> #include<iostream> #include<cstdlib> #include<algorithm> #define fastcall __attribute__((optimize("-O3"))) #define IL __inline__ __attribute__((always_inline)) using namespace std; typedef long long LL; const int maxn=210; const int mod=1e8+7; int X,Y,x,y,t,n,m,N; int C[maxn][maxn]; int a[maxn],b[maxn]; LL Ans; struct Matrix{ int a[maxn][maxn]; fastcall IL void clear(){memset(a,0,sizeof(a));} }A,ans; fastcall IL Matrix operator *(const Matrix &A,const Matrix &B){ Matrix C;C.clear(); for(int i=0;i<=N;++i){ for(int j=0;j<=N;++j){ LL tmp=0; for(int k=0;k<=N;++k){ tmp=tmp+1LL*A.a[i][k]*B.a[k][j]; }C.a[i][j]=tmp%mod; } }return C; } fastcall IL Matrix pow_mod(Matrix v,int p){ Matrix tmp;tmp.clear(); for(int i=0;i<=N;++i)tmp.a[i][i]=1; while(p){ if(p&1)tmp=tmp*v; v=v*v;p>>=1; }return tmp; } fastcall IL int Get_pow(int v,int p){ int tmp=1; while(p){ if(p&1)tmp=1LL*tmp*v%mod; v=1LL*v*v%mod;p>>=1; }return tmp; } int main(){ freopen("sea.in","r",stdin); freopen("sea.out","w",stdout); scanf("%d%d%d%d%d",&x,&y,&t,&n,&m); X=x+y;Y=x-y;C[0][0]=1;N=n+m; for(int i=1;i<=N;++i){ C[i][0]=1; for(int j=1;j<=i;++j){ C[i][j]=C[i-1][j-1]+C[i-1][j]; if(C[i][j]>=mod)C[i][j]-=mod; } } for(int i=0;i<=N;++i){ for(int j=i;j>=0;j-=2){ A.a[i][j]=(C[i][j]<<1); if(A.a[i][j]>=mod)A.a[i][j]-=mod; } } A=pow_mod(A,t); ans.clear(); for(int i=0;i<=N;++i)ans.a[i][0]=Get_pow(X,i); ans=A*ans; for(int i=0;i<=N;++i)a[i]=ans.a[i][0]; ans.clear(); for(int i=0;i<=N;++i)ans.a[i][0]=Get_pow(Y,i); ans=A*ans; for(int i=0;i<=N;++i)b[i]=ans.a[i][0]; for(int i=0;i<=n;++i){ int cnt=1; for(int j=0;j<=m;++j){ Ans=Ans+1LL*a[N-i-j]*b[i+j]%mod*C[n][i]%mod*C[m][j]*cnt; cnt=-cnt; }Ans=(Ans%mod+mod)%mod; } Ans=Ans*Get_pow((mod+1)>>1,N)%mod; printf("%lld ",Ans); return 0; }
第三题
第三题是今天失误最大的题目
考后想了想发现自己就算想不出来O(n^2)的DP
也能想出来O(n^3),O(n^4)的DP,然后顺着思考下去进行优化没准就能A了
然而考试的时候总是想着一口吃个胖子,一直想O(n^2)的状态
最后没时间了也只能写O(2^n)拿了20分收场
做法是我们注意到每个区间要么是一个前缀,要么是一个后缀
设f(i,j)表示处理了前i列,且打掉了j个后缀区间的方案数
转移的时候只需要讨论当前这一列是否打掉一个后缀区间就可以了
设当前可以打掉的后缀区间为k,可以得到
f(i,j)=f(i-1,j)+f(i-1,j-1)*(k-j-1)
之后我们考虑对于前缀区间的处理,不难发现若当前前缀区间右端点为i
那么我们之前任意一个空着没打的列都可以解决它
不妨只处理右端点为i的区间,设右端点<i的区间有k个
则f(i,j)=f(i,j)*(i-k-j-c) (c是当前处理了多少个右端点为i的区间)
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #include<cstdlib> using namespace std; const int maxn=3010; const int mod=998244353; int n,m,L,R; int A[maxn],B[maxn]; int f[maxn][maxn]; void read(int &num){ num=0;char ch=getchar(); while(ch<'!')ch=getchar(); while(ch>='0'&&ch<='9')num=num*10+ch-'0',ch=getchar(); } int main(){ freopen("attack.in","r",stdin); freopen("attack.out","w",stdout); read(n);read(m); if(m<2*n){printf("0 ");return 0;} for(int i=1;i<=n;++i){ read(L);read(R); A[L]++;B[R]++; } for(int i=1;i<=m;++i)B[i]+=B[i-1],A[i]+=A[i-1]; f[0][0]=1; for(int i=1;i<=m;++i){ f[i][0]=f[i-1][0]; int lim=min(i-A[i],B[i]); for(int j=1;j<=lim;++j){ f[i][j]=f[i-1][j]+1LL*f[i-1][j-1]*(B[i]-j+1)%mod; if(f[i][j]>=mod)f[i][j]-=mod; } for(int j=A[i-1]+1;j<=A[i];++j){ for(int k=0;k<=lim;++k){ f[i][k]=1LL*f[i][k]*(i-k-j+1)%mod; } } }printf("%d ",f[m][n]); return 0; }
第四题
一开始看的时候以为是HE省选的丝薄题
结果写了一发之后发现样例不对,思考了一会人生之后发现每个点只能被覆盖一次QAQ
这样样例就对了,然而并不会做了
发现n和m很小,于是就强行写了个插头DP,时间复杂度非常的玄学,最后跑了90分
正解还是网络流,但是不知道比HE的省选题高明到哪里去了QAQ
考虑到每个非障碍只会被覆盖一次,等价于这个点要么横着炸要么竖着炸
答案实际上是 (横或者竖着碰墙的个数+相邻两个格子属性不同的对数)/2
因为每个炸弹条都是有两个端点的QAQ
然后就转化成了二元关系最小割,用S表示属性为横,T表示属性为竖
如果一个点竖着会碰墙就从S到他连边,横着会碰墙就从他向T连边
相邻的格子之间连边,不难发现每一种割的方案都代表一种炸弹条的方案
求最小割即可
#include<cstdio> #include<cstring> #include<iostream> #include<cstdlib> #include<algorithm> #include<queue> using namespace std; const int oo=0x7fffffff; int n,m,tot,S,T,ans; char M[52][52]; int id[52][52]; int h[2510],cnt=1; int cur[2510],vis[2510]; struct edge{ int to,next,w; }G[3000010]; queue<int>Q; void add(int x,int y,int w1,int w2){ ++cnt;G[cnt].to=y;G[cnt].next=h[x];G[cnt].w=w1;h[x]=cnt; ++cnt;G[cnt].to=x;G[cnt].next=h[y];G[cnt].w=w2;h[y]=cnt; } void read(){ while(scanf("%s",M[n+1]+1)!=EOF)n++; m=strlen(M[1]+1); } void make_Graph(){ memset(id,-1,sizeof(id)); for(int i=1;i<=n;++i)for(int j=1;j<=m;++j)if(M[i][j]!='.')id[i][j]=++tot; S=0;T=tot+1; for(int i=1;i<=n;++i){ for(int j=1;j<=m;++j){ if(M[i][j]!='.'){ if(id[i+1][j]==-1||id[i-1][j]==-1){ int w=(id[i+1][j]==-1)+(id[i-1][j]==-1); add(S,id[i][j],w,0); } if(id[i][j-1]==-1||id[i][j+1]==-1){ int w=(id[i][j-1]==-1)+(id[i][j+1]==-1); add(id[i][j],T,w,0); } if(id[i+1][j]!=-1)add(id[i][j],id[i+1][j],1,1); if(id[i][j+1]!=-1)add(id[i][j],id[i][j+1],1,1); } } }return; } bool BFS(){ Q.push(S); for(int i=S;i<=T;++i)vis[i]=-1; vis[S]=0; while(!Q.empty()){ int u=Q.front();Q.pop(); for(int i=h[u];i;i=G[i].next){ int v=G[i].to; if(vis[v]==-1&&G[i].w>0){ vis[v]=vis[u]+1; Q.push(v); } } }return vis[T]!=-1; } int DFS(int x,int f){ if(x==T||f==0)return f; int w,used=0; for(int i=cur[x];i;i=G[i].next){ if(vis[G[i].to]==vis[x]+1){ w=f-used; w=DFS(G[i].to,min(w,G[i].w)); G[i].w-=w;G[i^1].w+=w; if(G[i].w>0)cur[x]=i; used+=w;if(used==f)return used; } } if(!used)vis[x]=-1; return used; } void dinic(){ while(BFS()){ for(int i=S;i<=T;++i)cur[i]=h[i]; ans+=DFS(S,oo); }return; } int main(){ freopen("tower.in","r",stdin); freopen("tower.out","w",stdout); read(); make_Graph(); dinic(); printf("%d ",ans>>1); return 0; }
今天考试的题目充分暴露了我的弱点:
1、看到第二题类比PKU校赛的H题其实想到了矩阵乘法,但是由于x和y不可拆分只好作罢
这表示我对坐标转换这类做法的题目掌握不够熟练
2、第三题死磕了很长时间的O(n^2)的状态
实际上我只要想到300的数据可以设计f(i,j,k)表示处理了前i列,前缀区间解决了j个,后缀区间解决了k个的方案数这个状态
转移是非常好写,这样就可以拿到70分,今天也不至于考的很糟糕
如果我写了这个DP其实不难发现第一维状态和第二维状态之间有转化关系
没准考场上时间多一点可以优化出正解QAQ
总的来说,自己的DP还是不够熟练,在不够熟练的基础上还要去想直接搞出正解QAQ自己也是作死
其实以后考试可以尝试着先转化出多项式的算法再一步步的想状态优化的做法
我觉得这个可能更适合我
留下的一些坑:DP专项,坐标转换,网络流相关(发现自己好久没有碰过网络流了
不知不觉留了好多坑,也不知道什么时候哪一天可以不考试让窝来填坑
感觉不学点新姿势自己吃枣药丸啊