博弈论、贪心、动态规划
Easy Version
考虑动态规划,设计 $dp_{i,j,k}$ 为第 $i$ 次选择格子 $(j,k)$ 是否必胜。
初始化 $dp_{l}$ 的所有元素,对于 $dp_{l,j,k}$ 如果 $a_l = b_{j,k}$ 就令 $dp_{l,j,k} = 1$;否则令 $dp_{l,j,k} = 0$。
然后倒序转移,对于 $dp_{i,j,k}$ 如果存在 $a_i = b_{j,k}$ 且对于所有 $j'>j,k'>k$ 都有 $dp_{i+1,j',k'} = 0$ 则 $dp_{i,j,k} = 1$;否则 $dp_{i,j,k} = 0$。
二维前缀和优化,时间复杂度 $O(n^3)$。没用上值域限制,怎么会是呢?
#include<bits/stdc++.h>
using namespace std;
bool ans,sum[305][305],dp[305][305][305];
int n,m,l,a[305],b[305][305];
void calculate(int x){
for(int i=1;i<=n+1;i++)
for(int j=1;j<=m+1;j++) sum[i][j]=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++) sum[i][j]=dp[x+1][i][j];
for(int i=1;i<=n;i++)
for(int j=m;j>=1;j--) sum[i][j]|=sum[i][j+1];
for(int i=n;i>=1;i--)
for(int j=1;j<=m;j++) sum[i][j]|=sum[i+1][j];
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++) dp[x][i][j]=(a[x]==b[i][j]&&!sum[i+1][j+1]);
}
void solution(){
cin>>l>>n>>m,ans=0;
for(int i=1;i<=l;i++) cin>>a[i];
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++) cin>>b[i][j];
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++) dp[l][i][j]=(a[l]==b[i][j]);
for(int i=l-1;i>=1;i--) calculate(i);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++) ans|=dp[1][i][j];
cout<<(ans ? 'T':'N')<<'\n';
}
int T;
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>T;
while(T--) solution();
return 0;
}
Hard Version
简单版没有用到值域限制的性质,但是根据 CF 的一贯风格,这个性质大概率是有用的,考虑它有什么用以及如何用。
性质:对于 $a$ 中重复出现的元素 $x$,删除第二次出现的 $x$ 和它之后的所有数字不影响答案。
现在证明在第一次选择 $x$ 时选择 $i+j$ 最大的 $b_{i,j} = x$ 不劣。首先这样会导致后续没有 $x$ 可选。
考虑 E1 的动态规划,注意到 $dp$ 的贡献实际上为前缀和形式,考虑修改状态定义:$dp_{i,j,k}$ 为第 $i$ 次在以 $(j,k)$ 为左上角的矩形中选择是否必胜,实际上这个是 E1 代码中 $sum$ 数组的定义。因为一旦某一步选择一个位置是必胜的,所有这一步能选这个位置的状态都是必胜的。
为了让对手能够达到的必胜态最少,当前应该选择一个最靠右下角的 $x$,同时如果选择一个不是最靠右下角的 $x$ 能够胜利,选择最靠右下角的 $x$ 同样能够胜利。于是第二次有人要选择的时候 $x$ 就没活了,这等于 $a$ 在 $x$ 第二次出现的位置结束。
根据这条性质可以得到一个元素不重复的序列 $a$,注意不是去重后的序列 $a$,有的元素可能直接没了。
由于状态是前缀和形式,只需要记录每一行最后一个等于 $1$ 的状态的下标,继续修改状态定义,设计 $dp_{i,j}$ 为第 $i$ 轮中原来 $dp$ 数组第 $j$ 列最后一个等于 $1$ 的状态的下标。
枚举 $a_i$ 对应的所有位置进行转移,由于 $a$ 中元素不重复,枚举的时间复杂度为 $O(n^2)$。初始化 $dp_{l}$ 的值,若 $dp_{1,1} \neq 0$ 则先手必胜,否则先手必败。
总体时间复杂度 $O(n^2)$。
#include<bits/stdc++.h>
using namespace std;
int n,m,l,a[1'505],b[1'505][1'505],dp[1'505][1'505];
vector<pair<int,int>> v[3'000'005];
bool on[3'000'005];
void calculate(int x){
for(auto [i,j]:v[a[x]]){
if(dp[x+1][i+1]>j) continue;
dp[x][i]=max(dp[x][i],j);
}
for(int i=n-1;i>=1;i--) dp[x][i]=max(dp[x][i],dp[x][i+1]);
}
void solution(){
cin>>l>>n>>m;
for(int i=1;i<=l;i++) cin>>a[i];
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++) cin>>b[i][j];
for(int i=1;i<=l;i++){
if(on[a[i]]) l=i-1;
on[a[i]]=1;
}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++) v[b[i][j]].push_back({i,j});
for(int i=l;i>=1;i--) calculate(i);
cout<<(dp[1][1]!=0 ? 'T':'N')<<'\n';
for(int i=1;i<=n*m;i++) on[i]=0,v[i].clear();
for(int i=1;i<=l;i++)
for(int j=1;j<=n;j++) dp[i][j]=0;
}
int T;
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>T;
while(T--) solution();
return 0;
}