CF1237F Balanced Domino Placements
基于观察不难发现骨牌横着放会一起占两列,占一行;竖着放会占两行,占一列。
因为原问题过于具体,考虑抽象一下这个问题。
不难发现问题可以很简单地被抽象为每一行可以一次选择连续的两行放一个物品 \(A\),或者选一行放一个物品 \(B\)。
那么问题就转化成行和列都要选择放若干个物品 \(A, B\) 且行放的 \(A\) 的数量等于列放的 \(B\) 的数量,行放的 \(B\) 的数量等于列放的 \(A\) 的数量。
但是你会发现这样一个放物品的过程会对应原来很多个放骨牌的方案,因为行与列之间 \(A, B\) 可以配对。
并且,对于任意一个配对方案都对应着原来放骨牌的一个方案。
可以发现行放的 \(A, B\) 之间配对是不影响的,因此配对的方案数应该是 \(|A|!|B|!\)。
于是我们就只需要考虑有多少种选择上述的选择物品 \(A, B\) 的方法了。
不难发现的是,行和列是独立开来的,因此我们可以分开考虑行和列的方案数,然后通过乘法原理相乘。下面以计算行的方案数为例。
首先不难发现一个暴力,令 \(f_{i, j, k}\) 表示当前考虑完前 \(i\) 行,\(A\) 放了 \(j\) 个,\(B\) 放了 \(k\) 个的方案数。
但是状态是 \(n ^ 3\) 的,显然不可取,只能从状态上下手优化。
你会发现实际上 \(B\) 的放置方法大可不必如此计算,因为只要有一个空位就能放一个 \(B\),我们只要知道最后的空位数量就可以直接计算出放 \(k\) 个 \(B\) 的方案数。
那么可以考虑令 \(f_{i, j}\) 表示考虑完前 \(i\) 行,\(A\) 放了 \(j\) 个且还没有放 \(B\) 的方案数,转移是 \(O(1)\) 的。
那么最后可以放 \(B\) 的空位就有 \(cnt - 2 \times j\)(其中 \(cnt\) 为除去题目中已放置的骨牌还剩下的行数),放 \(k\) 个 \(B\) 就有 \(\dbinom{cnt - 2 \times j}{k}\) 种方案。
最后合并时因为只需要枚举行选了多少个 \(A\) 多少个 \(B\) 就可以确定列选择的数量,因此合并的复杂度也是 \(O(n ^ 2)\) 的。
于是我们就在 \(O(n ^ 2)\) 的时间复杂度内完成了这个问题。
#include <bits/stdc++.h>
using namespace std;
#define rep(i, l, r) for (int i = l; i <= r; ++i)
const int N = 3600 + 5;
const int Mod = 998244353;
bool row[N], com[N];
int n, m, k, x, y, L, rc, cc, xx, yy, ans, fac[N], f[N][N], g[N][N], C[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 Inc(int a, int b) { return (a += b) >= Mod ? a - Mod : a;}
int Mul(int a, int b) { return 1ll * a * b % Mod;}
int main() {
n = read(), m = read(), k = read();
rep(i, 1, k) {
x = read(), y = read(), xx = read(), yy = read();
row[x] = row[xx] = com[y] = com[yy] = 1, --rc, --cc;
if(x != xx) --rc; if(y != yy) --cc;
}
rc += n, cc += m;
L = max(n, m), fac[0] = C[0][0] = 1;
rep(i, 1, L) fac[i] = Mul(fac[i - 1], i), C[i][0] = 1;
rep(i, 1, L) rep(j, 1, i) C[i][j] = Inc(C[i - 1][j - 1], C[i - 1][j]);
f[0][0] = 1;
rep(i, 1, n) rep(j, 0, i / 2) {
f[i][j] = Inc(f[i][j], f[i - 1][j]);
if(i >= 2 && !row[i] && !row[i - 1]) f[i][j] = Inc(f[i][j], f[i - 2][j - 1]);
}
g[0][0] = 1;
rep(i, 1, m) rep(j, 0, i / 2) {
g[i][j] = Inc(g[i][j], g[i - 1][j]);
if(i >= 2 && !com[i] && !com[i - 1]) g[i][j] = Inc(g[i][j], g[i - 2][j - 1]);
}
rep(i, 0, rc / 2) rep(j, 0, (cc - i) / 2) if(2 * i + j <= rc && i + 2 * j <= cc){
ans = Inc(ans, Mul(f[n][i], Mul(g[m][j], Mul(C[rc - 2 * i][j], Mul(C[cc - 2 * j][i], Mul(fac[i], fac[j]))))));
}
printf("%d", ans);
return 0;
}
可以发现,注意到两点对解出本题至关重要:
-
具体问题抽象化,转化问题。
-
发现行和列是互相独立的,可以分开计算然后合并。
本质上这两点都是思考了问题的反面,是正难则反的应用。