从 P11816 初学 Hall 定理和轮廓线 dp
知识点:Hall 定理、轮廓线 dp。
赛时被队友一眼秒了但是自己不会怎么办。
本文内棋子可以移动的方向和题目里的相反。
我们把初始状态的每个棋子看作一个左部点,最终状态的每个棋子看作一个右部点。
在可以到达的状态之间连边,本题就是在求这个二分图是否存在完备匹配。
根据 Hall 定理,一个二分图存在完备匹配的充要条件是对于左部点的大小为
所以我们令
选取若干个点,如果有一个点被某个选择的点偏序了那么这个点也要选。使得选出的点的权值
如果这个最大值大于
设
因为后两维的数据范围很小,考虑直接把他们压缩在一起。状态数的计算考虑组合意义,即
有第一种状态
直接暴力转移是
怎么办?可以考虑轮廓线 dp。
| 假如轮廓线现在的状态长这样: | |||||||
|---|---|---|---|---|---|---|---|
那么我们枚举问号处选中了多少个点,就可以从上一个轮廓线的状态转移。
还是因为偏序关系,这里轮廓线的总状态数顶天是
实现时建议开滚动数组,以及特判
代码前面几行也是简要的题解。
//学习Hall定理和轮廓线dp。
//首先每个棋子看成一个点 a是左部点b是右部点
//能移动到的连边 这题就是在求是否有完备匹配
//Hall定理:一个图存在完备匹配的充要条件是
// 对于任意大小为k的左部点集S,它的可达右部点集T大小>=k
//那么令c[i][j][k]=a[i][j][k]-b[i][j][k]
//我们要选择一些点(如果一个点选了那么被偏序的必须选)
//使得sum(c[i][j][k])最大。
//其他两维很小,考虑直接压缩状态
// #####. 首先对于一层的点的选择状态肯定长这样。
// #####. 考虑直接把第三维压掉变成二维问题。
// ####.. 从上往下转移,每层选的点数肯定也和上一层偏序。
// ####.. 然后转移是一个高维前缀max 但是不好做 实际上和状压差不多
// ##.... 我们考虑轮廓线dp!
// 假如轮廓线现在长这样(存储为f[4][6,3,3,2,1,0])
// ...?10 那么我们只需要枚举?位置的值就可以从f[3][6,3,3,?,1,0]转移过来了
// 6332 用组合意义粗略估算有效状态数约为C(12,6)*6=5544。轻微卡常。
#include<bits/stdc++.h>
#define MX 10005
#define int long long
#define CKE if(CHECK)
#define FRE if(FIL)
using namespace std;
const int CHECK=0,FIL=0;int read();
int T,n,x,y,cnt,a[MX][7][7],b[MX][1000];
int f[117649],p[1000][8],q[7],g[7][1000];
void dfs(int t,int lst,int o){
if(t>x){
p[++cnt][0]=o;f[o]=cnt;
for(int i=1;i<=x;i++)
p[cnt][i]=q[i];
p[cnt][x+1]=0;
return;
}
for(int i=0;i<=lst;i++){
q[t]=i;dfs(t+1,i,o*(y+1)+i);}
}
signed main(){
//ios::sync_with_stdio(false);
//cin.tie(0);cout.tie(0);
FRE freopen(".in","r",stdin);
FRE freopen(".out","w",stdout);
T=read();while(T--){
n=read();x=read();y=read();
for(int i=n;i>=1;i--) for(int j=x;j>=1;j--) for(int k=y;k>=1;k--)
a[i][j][k]=read();
for(int i=n;i>=1;i--) for(int j=x;j>=1;j--) for(int k=y;k>=1;k--)
a[i][j][k]-=read();
//搜索所有可能状态 & 其他预处理
//p[t][0] 第t个状态hash值
//p[t][1~x] 第t个状态代表什么
//f[h] hash值为h的是哪个状态
//b[i][t] 第i层 第t个状态覆盖的点的权值和
cnt=0;dfs(1,y,0);
memset(g[0],-0x3f,sizeof(g[0]));
g[0][1]=0,q[x]=1;
for(int i=x-1;i>=0;i--) q[i]=q[i+1]*(y+1);
for(int i=1;i<=n;i++){
for(int j=1;j<=cnt;j++){
b[i][j]=0;
for(int k=1;k<=x;k++) for(int l=1;l<=p[j][k];l++)
b[i][j]+=a[i][k][l];
}
}
//轮廓线 dp
int res=0;
for(int t=n;t>=1;t--){
for(int o=1;o<=x;o++){
for(int I=1;I<=cnt;I++){
//第t块
//轮廓线包含第t块前o个和第t-1块的后B-o个状态
//这些状态叠起来是第I个状态
int lo=p[I][o+1];
int hi=p[I][o];
int zt=p[I][0]-q[o]*hi;
if(x>1) g[o%x][I]=-1e18;
for(int i=lo;i<=hi;i++)
g[o%x][I]=max(g[o%x][I],g[o-1][f[zt+q[o]*i]]);
}
}
//这一层枚举完了 加上对应权值
for(int I=1;I<=cnt;I++){
g[0][I]+=b[t][I];
if(t==1) res=max(res,g[0][I]);
}
}
if(res>0) printf("NIE\n");
else printf("TAK\n");
}
return 0;
}
int read(){
int Ca=0;char Cr=' ';int Cf=1;
while(Cr<'0' || Cr>'9'){Cr=getchar();if(Cr=='-'){Cf=-1;}}
while(Cr>='0' && Cr<='9'){Ca=Ca*10+Cr-48;Cr=getchar();}
return Ca*Cf;
}