8月29日考试 题解(区间DP+贪心+并查集+博弈论)
T1区间DP写挂了挂掉40分,一开始写的对着呢QAQ。$rk2$变成$rk6$了。
T1合并集合
有一个长度为$n$的呈环状的集合,每个集合记为$S_i$。合并一个集合所产生的分数为$size_i \times size_j$,合并后集合内的元素都是互不相同的。问最大分数。
显然区间DP。套路题,破环成链,然后$n^3$搞就好。
代码:
#include<bits/stdc++.h> #define int long long using namespace std; const int maxn=305; int a[maxn*2],n,vis[maxn*2],ans,cnt[maxn*2][maxn*2],f[maxn*2][maxn*2]; inline int read() { int x=0,f=1;char ch=getchar(); while(!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();} while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();} return x*f; } signed main() { //freopen("merge.in","r",stdin); //freopen("merge.out","w",stdout); n=read(); for (int i=1;i<=n;i++) a[i]=read(),a[i+n]=a[i]; for (int i=1;i<=2*n;i++) for (int j=i;j<=2*n;j++) { memset(vis,0,sizeof(vis)); for (int k=i;k<=j;k++) if (!vis[a[k]]) vis[a[k]]=1,cnt[i][j]++; } for (int len=2;len<=n;len++) for (int l=1;l<=2*n-len+1;l++) { int r=l+len-1; for (int k=l;k<=r;k++) f[l][r]=max(f[l][r],f[l][k]+f[k+1][r]+cnt[l][k]*cnt[k+1][r]); } for (int i=1;i<=n;i++) ans=max(ans,f[i][i+n-1]); printf("%lld",ans); return 0; }
T2 climb
有一个人在井底,井高为$l$。他现在有$n$种向上爬的方法:每次每天上午上升$a_i$,晚上滑下$b_i$。井内水位一开始是0,每天上升$c_i$。如果如果某天晚上人的高度小于等于水位,那么他会被淹死。问至少在第几天爬出井,如果不能输出$-1$。
同机房大佬的做法太神仙了。我就写了一个贪心然后就A了。说一下贪心策略:
假设之前已经向上爬的高度为$len$,已经过了$day$天。
1.首先挑一个$a_i$最大的方案,看$len+a_i$是否大于等于$l$,如果合法,直接输出当前天数。
2.如果不合法,那么挑一个$a_i-b_i$最大的;如果有很多个最大的,挑一个$a_i$最小的(因为有可能后面利用较大的$a_i$能爬出井)。
3.每天结束后判断一下这个人有没有被淹死即可。
代码:
#include<bits/stdc++.h> #define int long long using namespace std; const int maxn=100005; int n,c[maxn],water,man,cnt1=1,cnt2=1,vis[maxn],l; struct node { int x,y,z,id; }a[maxn]; struct h { int x,y,id; }b[maxn]; inline int read() { int x=0,f=1;char ch=getchar(); while(!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();} while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();} return x*f; } bool cmp(node a,node b) { if (a.z==b.z) return a.x<b.x; return a.z>b.z; } bool cmp1(h a,h b) { return a.x>b.x; } signed main() { //freopen("climb.in","r",stdin); //freopen("climb.out","w",stdout); n=read();l=read(); for (int i=1;i<=n;i++) { a[i].x=read();a[i].y=read(); b[i].x=a[i].x;b[i].y=a[i].y; a[i].z=a[i].x-a[i].y; a[i].id=b[i].id=i; } for (int i=1;i<=n;i++) c[i]=read(); sort(a+1,a+n+1,cmp); sort(b+1,b+n+1,cmp1); for (int i=1;i<=n;i++) { while(vis[b[cnt2].id]) cnt2++; if (man+b[cnt2].x>=l){printf("%lld",i);return 0;} else man+=a[cnt1].z,vis[a[cnt1++].id]=1; water+=c[i]; if (man<=water){printf("-1");return 0;} } printf("-1"); return 0; }
T3 coin
给一个$n*m$的棋盘,每个格子上至多只有一个硬币。硬币或正或反。每次可以选择没有被选择过的一行或一列,将上面的所有硬币都翻过来。当所有行或列都被选择过或者硬币全为正面朝上时,游戏结束。现在两个人轮流进行操作,游戏结束时最后执行操作的人将获得$1$分;如果硬币全为正面朝上,那么两个人都将额外获得$2$分。两个人都想让自己的得分尽可能大,且都按最优策略操作。问先手的分数。
首先发现一个显然的事实:如果想让硬币都正面朝上,那么原来正面朝上的硬币应被翻偶数次,反面朝上的硬币应被翻奇数次。
每一行或每一列都可选择翻或不翻,那么对于每个硬币所在的位置$(i,j)$,硬币的状态对应了$i$和$j$有着某种对应关系。我们考虑$2-SAT$的思想:
我们记选为$1$,不选为$0$。对于行$i$和列$j$,它们都可以选或不选,对应过来为$i,i',j,j'$。
如果硬币正面朝上,那么$i$向$j$连边,$i'$向$j'$连边(都是无向边),表示行和列要么都不翻,要么都得翻。
如果硬币反面朝上:则与上面相反。道理一样。
因为是无向边,所以直接并查集维护连通块。无解的情况为一个点选和不选的状态在一个连通块中。此时直接输出$(n+m)\ \mod \ 2$。
如果有解,则我们考虑连通块的性质。因为是无向边,且每个点都有两种状态,所以必有一对连通块是对称(点数相同且颜色相反)的。我们把含$1$个数奇偶性相同的一对连通块称为“方块”,不同的一对记为“三角”。如果三角个数为奇数,那么先手胜;如果为偶数则看方块内$1$的总数。如果为奇数先手必胜;否则先手必败。
代码:
#include<bits/stdc++.h> using namespace std; const int maxn=505; int fa[maxn],n,m,t,size[maxn],vis[maxn],cnt1,cnt2,flag; char a[105][105]; inline int find(int x) { if (x==fa[x]) return x; return fa[x]=find(fa[x]); } inline void merge(int x,int y) { int xx=find(x),yy=find(y); if (xx==yy) return; fa[yy]=xx; size[xx]+=size[yy]; } inline void work(int x) { int y=find(x+n+m);x=find(x); vis[x]=vis[y]=1; if (size[x]%2!=size[y]%2) cnt1++; else cnt2+=(size[x]%2); } int main() { cin>>t; while(t--) { cin>>n>>m; cnt1=0,cnt2=0,flag=0; for (int i=1;i<=2*(n+m);i++) { fa[i]=i; if (i<=n+m) size[i]=1; else size[i]=0; vis[i]=0; } for (int i=1;i<=n;i++) { scanf("%s",a[i]+1); for (int j=1;j<=m;j++) if (a[i][j]=='x') merge(i,j+n+n+m),merge(i+n+m,j+n); else if (a[i][j]=='o') merge(i,j+n),merge(i+n+m,j+n+n+m); } for (int i=1;i<=n+m;i++) if (find(i)==find(i+n+m)){flag=1;break;} if (flag){printf("%d\n",(n+m)%2);continue;} else { for (int i=1;i<=n+m;i++) if (find(i)==i) work(i); if (cnt1%2==1) printf("3\n"); else printf("%d\n",2+(cnt2%2)); } } return 0; }