[九省联考2018]一双木棋
因为本题是两个人同时以自己最优的方式在进行博弈,因此是不能使用贪心来求解的,只能使用 \(dp\)。
首先不难发现如果一个位置 \((x, y)\) 能落子那么 \((1, 1) \sim (x, y)\) 中的所有位置必须已经落子。
那么每一个合法的状态都会形成一个阶梯状,从上到下往右扩展的长度不增。
那么一个暴力的想法就是记录每一行的状态,但显然这样是不能通过本题的,需要另辟蹊径。
但我们依然只能在状态上下手脚,在 AT1975 [ARC058C] 和風いろはちゃん 中出现过将十进制数压成 \(2\) 进制的方法,不妨来试一试。
但你会发现这样的复杂度还是 \(O(2 ^ {nm})\) 的,甚至劣于原先的复杂度。
但转念一想,本题和那道题不同的地方在于这道题填进去的数是依次递减的。
这意味着所有增量之和是不超过 \(m\) 的!
于是我们可以将这个高度从上到下差分,每次插入进去增量的长度即可,每一行之间使用 \(0\) 分隔开。
具体来说,每新加入一行,我们先在字符串末尾加入一个 \(0\) 表示分隔,其次在加入若干个 \(1\) 数量为这一行的长度减下一行的长度。
于是这样我们描述状态的复杂度就降至 \(O(2 ^ {n + m})\) 了。
那么就只要考虑转移即可,我们简单记作 \(dp_S\) 表示状态为 \(S\) 时先手得分减后手得分的值。
首先你可以发现每次将落下一个子后,如果加入的是第一行,相当于在最后的 \(10\) 之间插入了一个 \(1\) 状态值变大。
如果放在最后一行,显然是在第一个 \(1\) 后添加一个 \(0\),状态值也会变大。
如果放在中间的一行,相当于将 \(S\) 中的 \(01\) 交换成 \(10\),状态值也会变大。
因此我们就发现了 \(dp\) 的拓扑序,也方便了下面的转移。
但你会发现正着 \(dp\) 貌似是不行的,因为实质上一个人让自己最优后一定会让对手走入当前状态,而正着 \(dp\) 是不能继续接着上面的状态的。
但是你会发现反过来 \(dp\) 刚好能使得后手进入的状态必定是先手决策以后的状态,因此对于这种对抗博弈,我们一般考虑反着 \(dp\)。
#include <bits/stdc++.h>
using namespace std;
#define rep(i, l, r) for (int i = l; i <= r; ++i)
#define dep(i, l, r) for (int i = r; i >= l; --i)
const int N = 20 + 5;
const int M = (1 << 20) + 5;
const int inf = 1e9;
int n, m, maxl, final, dp[M], sta[M], cnt[M], a[N][N], b[N][N];
int read() {
char c; int x = 0, f = 1;
c = getchar();
while (c > '9' || c < '0') { if(c == '-') f = -1; c = getchar();}
while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
return x * f;
}
int Count(int x) { int ans = 0; for (; x; x >>= 1) if(!(x & 1)) ++ans; return ans;}
void update(int v, int u, int x, int y) {
if(dp[v] == inf || dp[v] == -inf) return;
if(sta[v]) dp[u] = max(dp[u], dp[v] + a[x][y]);
else dp[u] = min(dp[u], dp[v] - b[x][y]);
}
int main() {
n = read(), m = read(), maxl = (1 << (n + m)) - 1;
rep(i, 1, n) rep(j, 1, m) a[i][j] = read();
rep(i, 1, n) rep(j, 1, m) b[i][j] = read();
rep(S, 0, maxl) {
int tmp = 0, num = 0;
dep(i, 0, n + m) {
if(S & (1 << i)) ++tmp;
else num += tmp;
}
dp[S] = (num & 1 ? inf : -inf), cnt[S] = Count(S), sta[S] = (num & 1);
}
rep(i, n, n + m - 1) final += (1 << i);
dp[final] = 0;
dep(S, 2, final - 1) {
int P = 1, tmp = 0, res = 0;
rep(i, 1, n + m - 1) if(S & (1 << i)) ++P;
if((S << 1) + 2 <= maxl && P <= m) update((S << 1) + 2, S, 1, P);
for (P = n + m - 1; P && !(S & (1 << P)); --P);
if(S - (1 << P) + (1 << (P + 1)) <= maxl && cnt[S] < n)
update(S - (1 << P) + (1 << (P + 1)), S, cnt[S] + 1, 1);
dep(i, 1, P) {
if(!(S & (1 << i))) {
if(S & (1 << (i - 1))) update(S + (1 << i) - (1 << (i - 1)), S, cnt[S] - tmp, res + 1);
++tmp;
}
else ++res;
}
}
printf("%d", dp[2] + a[1][1]);
return 0;
}
一般来说,压高维的字符串往往能写成压二进制串的形式,注意如果表达一个单调的序列可以记录差分数组,这样记录的总量级是序列最大值级别的。
注意对抗博弈需要倒着 \(dp\)。