状压 DP
按难度递增。
P5911 [POI 2004] PRZ
题意
有 \(n\) 个具有 \(t\) 和 \(w\) 两个属性的物品,将其分成若干组,要求每组的 \(w\) 之和不超过 \(W\),定义每组的花费为 \(t\) 的最大值,求每组花费之和的最小值。
数据范围:\(100 \le W \le 400, 1 \le n \le 16, 1 \le t \le 50, 1 \le w \le 100\)。
思路
前置知识:枚举子集。
从大到小枚举 \(S\) 的子集 \(S'\),显然初始 \(S' = S\)。设 \(S'\) 的最低位 \(1\) 位于第 \(k\) 位,则 \(S' - 1\) 将 \(S'\) 的第 \(k\) 位化为 \(0\),第 \(< k\) 位化为 \(1\)。
-
定理一:\((S' - 1) \& S\) 是小于 \(S'\) 的 \(S\) 的子集。显然,\(S\) 是 \(1\) 的位置,\((S' - 1) \& S\) 才有可能是 \(1\),而两元素按位与的结果小于或等于元素本身,得证。
-
定理二:不存在小于 \(S'\) 的 \(S\) 的子集大于 \((S' - 1) \& S\)。由于在小于 \(S'\) 的数中,\(S' - 1\) 在第 \(\ge k\) 位已经最大,第 \(< k\) 位全部为 \(1\),在对 \(S\) 按位与后,第 \(> k\) 位仍然最大,在第 \(> k\) 位相同时,第 \(\le k\) 位同样最大,得证。
-
于是,\((S' - 1) \& S\) 即为小于 \(S'\) 的最大的 \(S\) 的子集。枚举子集时,不断令 \(S' \leftarrow (S' - 1) \& S\),直到 \(S' = 0\),即可枚举 \(S\) 的全部非零子集。最后对 \(0\) 特判一下即可,时间复杂度 \(O(2^w)\),其中 \(w\) 为 \(S\) 在二进制下 \(1\) 的数量。
设 \(dp(S)\) 表示状态 \(S\) 已经被分过组的每组花费之和的最小值。令 \(f(S)\) 表示按状态 \(S\) 分组的花费,\(g(S)\) 表示按状态 \(S\) 分组的 \(w\) 之和。
枚举 \(S\) 的真子集 \(S'\),令 \(S'\) 为原本的状态,则 \(S \oplus S'\) 为最后一次分组的选择状态,\(\oplus\) 表示二进制下按位异或。则有:
设 \(k(S)\) 为 \(S\) 在二进制下 \(1\) 的数量,则时间复杂度为 \(O(\sum\limits_{S} 2^{k(S)})\)。
在二进制下有 \(k\) 个 \(1\) 的 \(S\) 的数量为 \(\binom{n}{k}\),则时间复杂度为 \(O(\sum\limits_{k = 0} ^ n \binom{n}{k} 2^k)\)。
二项式定理:\((x + y)^n = \sum\limits_{k = 0}^n \binom{n}{k}x^k y^{n - k}\)。当 \(x = 2, y = 1\) 时,\(3^n = \sum\limits_{k = 0} ^ n \binom{n}{k} 2^k\)。
因此,时间复杂度 \(O(3^n)\)。
代码
点击查看代码
W = read(), n= read(), maxS = (1 << n);
for(int i = 0; i < n; i++) t[i] = read(), w[i] = read();
for(int S = 1; S < maxS; S++)
for(int i = 0; i < n; i++)
if((S >> i) & 1)
MAX(val[S].first, t[i]), val[S].second += w[i];
for(int S = 1; S < maxS; S++){
if(val[S].second <= W) dp[S] = val[S].first; // 空子集。
else dp[S] = 800;
for(int i = (S - 1) & S; i; i = (i - 1) & S)
if(val[S ^ i].second <= W)
MIN(dp[S], dp[i] + val[S ^ i].first);
}
print(dp[maxS - 1]);
P3977 [TJOI2015] 棋盘
题意
以下行、列皆从 \(0\) 开始。一个棋子的攻击范围用一个 \(3 \times p\) 的矩阵表示,其中 \(1\) 表示攻击得到,\(0\) 表示攻击不到,棋子默认在该矩阵的第 \(1\) 行,第 \(k\) 列。求在一个 \(n \times m\) 的棋盘中任意摆放棋子,使得每个棋子都不会被攻击到的方案数,对 \(2^{32}\) 取模。
数据范围:\(1 \le n \le 10^6, 0 \le k < p \le m \le 6\)。
思路
取模直接用 unsigned int 即可。设 \(dp(i, S)\) 表示对于第 \(0 \sim i\) 行,第 \(i\) 行的摆放状态为 \(S\) 的方案数。首先,求出所有的合法 \(S\),使得某一行的摆放状态为 \(S\) 时,该行内部没有棋子会被攻击到,则 \(dp(0, S) = 1\)。
然后,\(dp(i, B)\) 由 \(dp(i - 1, A)\) 转移,需要满足 \(A, B\) 之间互相合法。可以预处理合法状态对 \((A, B)\)。时间复杂度 \(O(n 4^m)\)。
考虑优化,注意到转移方程是固定的(取决于合法状态对 \((A, B)\)),考虑矩阵快速幂优化 DP。设转移矩阵为 \(T\),则 \(T\) 的第 \(A\) 行,第 \(B\) 列为 \(1\)。初始矩阵为 \(base\),\(base\) 的第 \(0\) 行,第 \(S\) 列为 \(1\)。
则,答案矩阵 \(res = base * T^{n - 1}\)。答案为 \(res\) 的第 \(0\) 行之和。时间复杂度 \(O(8^m \log n)\)。
代码
点击查看代码
#define unt unsigned int
struct mat{
int n; unt a[N][N];
mat(int org_n){
n = org_n;
for(int i = 0; i < n; i++)
for(int j = 0; j < n; j++)
a[i][j] = 0;
}
inline mat operator*(const mat t)const{
mat res(n);
for(int i = 0; i < n; i++)
for(int j = 0; j < n; j++)
for(int k = 0; k < n; k++)
res.a[i][j] += a[i][k] * t.a[k][j];
return res;
}
};
inline bool check(int A, int r, int B){
for(int j = 0; j < m; j++)
if((A >> j) & 1){
int t = j - (p - k - 1);
int val = (t > 0 ? ((att[r] << t) & B) : (((att[r] >> (-t)) & B)));
if(A == B && r == 1 && val != (1 << j)) return false;
if(!(A == B && r == 1) && val) return false;
}
return true;
}
inline mat ksm(mat a, int b){
mat res(1 << m);
for(int i = 0; i < (1 << m); i++) res.a[i][i] = 1;
while(b){
if(b & 1) res = res * a;
a = a * a, b >>= 1;
}
return res;
}
n = read(), m = read(), maxS = (1 << m);
p = read(), k = read();
for(int i = 0; i < 3; i++) for(int j = p; j; j--) att[i] += read() * (1 << (j - 1));
for(int S = 0; S < maxS; S++) if(check(S, 1, S)) legal.push_back(S);
mat T(1 << m);
for(auto A : legal)
for(auto B : legal)
T.a[A][B] = (check(A, 2, B) && check(B, 0, A));
mat base(1 << m);
for(auto S : legal) base.a[0][S] = 1;
mat res = base * ksm(T, n - 1);
unt ans = 0;
for(auto S : legal) ans += res.a[0][S];
print(ans);
P3959 [NOIP 2017 提高组] 宝藏
题意
有 \(n\) 个点,\(m\) 条带权无向边,形成连通图。对于原图的一颗边集为 \(E\) 的有根生成树,定义一条边的花费 \(cost(u, v)\) 为根到父亲路径上的点数。求 \(\sum\limits_{\{u, v, w\} \in E} w \times cost(u, v)\) 的最小值。
数据范围:\(1 \le n \le 12, 0 \le m \le 10^3, 1 \le w \le 5 \times 10^5\)。
思路
基于点深度考虑,根节点深度为 \(0\)。对于一颗给定的有根生成树,令 \(ask(i)\) 表示所有深度为 \(i\) 的点到其父亲的边权之和,则该树答案为 \(\sum\limits_{i = 1}^{n - 1} i * ask(i)\)。
枚举根节点,然后进行 DP。
设 \(dp(i, S)\) 表示目前树深为 \(i\),树内节点集合为 \(S\),的最小答案。转移:\(dp(i, S) = \min\limits_{A \bigcup B = S, A \bigcap B = \varnothing} dp(i - 1, A) + i \times c(A, B)\)。
其中,\(c(A, B)\) 表示将集合 \(B\) 中节点添加至集合 \(A\) 中的最小代价,显然可以预处理两点间最小边权、点添加至集合的最小代价,得到集合添加至集合的最小代价。
但是,如果将集合 \(B\) 中节点添加至集合 \(A\) 中,可能会让集合 \(B\) 中节点深度小于 \(i\),是否影响答案呢?
其实是不影响的,因为明明深度小于 \(i\) 了,算代价却乘以 \(i\),可能让代价偏大,但最优解情况不受影响,因此其仍然是最优解。
代码
点击查看代码
// a[u][v]: 点 u, v 间最小边权。
// b[u][S]: 点 u 到集合 S 内任一点的最小边权。
// c[A][B]: 集合 B 的点添加到集合 A 的最小边权和。
n = read(), m = read(), maxS = 1 << n;
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++) a[i][j] = inf;
a[i][i] = 0, p[1 << i] = i;
}
for(int i = 0; i < m; i++){
int u = read() - 1, v = read() - 1, w = read();
MIN(a[u][v], w), MIN(a[v][u], w);
}
for(int u = 0; u < n; u++)
for(int S = 0; S < maxS; S++){
if(!((S >> u) & 1)){
b[u][S] = inf;
for(int s = S; s; s -= (s & (-s)))
MIN(b[u][S], a[u][p[s & (-s)]]);
}
}
for(int S = 1; S < maxS; S++)
for(int A = (S - 1) & S; A; A = (A - 1) & S)
for(int s = A; s; s -= (s & (-s)))
c[S ^ A][A] += b[p[s & (-s)]][S ^ A];
for(int st = 0; st < n; st++){
for(int i = 0; i < n; i++)
for(int S = 0; S < maxS; S++)
dp[i][S] = inf;
dp[0][1 << st] = 0;
MIN(ans, dp[0][maxS - 1]);
for(int i = 1; i < n; i++){
for(int S = 0; S < maxS; S++)
if((S >> st) & 1)
for(int s = (S - 1) & S; s; s = (s - 1) & S)
MIN(dp[i][S], dp[i - 1][s] + i * c[s][S ^ s]);
MIN(ans, dp[i][maxS - 1]);
}
}
print(ans);
P3160 [CQOI2012] 局部极小值
题意
有一个 \(n \times m\) 的数字矩阵,构成了 \(1 \sim n \times m\) 的排列。若某格数字比其相邻格数字(存在公共边或公共顶点)小,则该格数字是局部极小值。给出所有局部极小值的位置(X 表示该格数字是局部最小值,. 表示该格数字不是局部最小值),判断合法矩阵的数量。
数据范围:\(1 \le n \le 4, 1 \le m \le 7\)。
思路
注意到任意两个 X 是不能相邻的,因此在 \(4 \times 7\) 的矩阵中 X 的数量 \(cntX\) 最多为 \(8\)。考虑状压 DP,设 \(dp(S, i)\) 表示从小到大填数,状态 \(S\) 的 X 已被填数,目前已填 \(1 \sim i\),未填 \(i + 1 \sim n \times m\),的方案数。
-
将 \(i + 1\) 填入
.:由于
X必然在其相邻格之前填数,故未填数的X的相邻格同样未填数,求出不属于未填数的X及其相邻格的格子数量 \(rest\),则目前填过的 \(1 \sim i\)(不论X或.)都在这 \(rest\) 个格子里,则 \(i + 1\) 有 \(rest - i\) 种选择,有:\[dp(S, i + 1) \stackrel{+}{\longleftarrow} dp(S, i) \times (rest - i) \] -
将 \(i + 1\) 填入
X:只能填入未填数的
X,枚举未填数的X为第 \(k\) 个,有:\[dp(S | (1 \ll k), i + 1) \stackrel{+}{\longleftarrow} dp(S, i) \]
则,\(dp((1 \ll cntX) - 1, n \times m)\) 即为包含题中给定的 X 的方案数。这并不是我们想要的,因为包含不等于恰好,可能会有 . 被错判为 X。
因此,设 \(g(t)\) 表示在包含题中给定的 X 的前提下,额外有 \(t\) 个 X 的方案数。则答案为:
考虑对原图进行 \(01\) DFS,搜索到当前位置时,令当前位置保持原样搜索一遍,从 . 变为 X 再搜索一遍(需判断改变是否合法)。时间复杂度 \(O(可过)\)。
代码
点击查看代码
inline bool check(int x, int y){
return x >= 0 && x < n && y >= 0 && y < m;
}
inline int DP(){
int cntX = 0, xX[8] = {0}, yX[8] = {0};
bool vis[4][7];
for(int x = 0; x < n; x++)
for(int y = 0; y < m; y++)
if(X[x][y])
xX[cntX] = x, yX[cntX] = y, cntX++;
for(int S = 0; S < (1 << cntX); S++)
for(int i = 0; i <= n * m + 1; i++)
dp[S][i] = 0;
dp[0][0] = 1;
for(int S = 0; S < (1 << cntX); S++){
for(int x = 0; x < n; x++)
for(int y = 0; y < m; y++)
vis[x][y] = false;
int rest = n * m;
for(int i = 0; i < cntX; i++)
if(!((S >> i) & 1)){
int nx = xX[i], ny = yX[i];
if(!vis[nx][ny]) vis[nx][ny] = true, rest--;
for(int t = 0; t < 8; t++){
nx = xX[i] + dx[t], ny = yX[i] + dy[t];
if(check(nx, ny) && !vis[nx][ny]) vis[nx][ny] = true, rest--;
}
}
for(int i = 0; i <= rest; i++)
if(dp[S][i]){
ADD(dp[S][i + 1], dp[S][i] * (rest - i) % mod);
for(int j = 0; j < cntX; j++)
if(!((S >> j) & 1))
ADD(dp[S | (1 << j)][i + 1], dp[S][i]);
}
}
return dp[(1 << cntX) - 1][n * m];
}
void dfs(int x, int y, int k){
if(x == n){
int res = DP();
return ADD(ans, (k ? res : mod - res));
}
int nx = x, ny = y + 1;
if(ny == m) ++nx, ny = 0;
dfs(nx, ny, k);
if(!X[x][y]){
for(int t = 0; t < 8; t++)
if(check(x + dx[t], y + dy[t]) && X[x + dx[t]][y + dy[t]])
return ;
X[x][y] = true;
dfs(nx, ny, k ^ 1);
X[x][y] = false;
}
}
n = read(), m = read();
for(int i = 0; i < n; i++){
scanf("%s", s[i]);
for(int j = 0; j < m; j++)
if(s[i][j] == 'X')
X[i][j] = true;
}
for(int x = 0; x < n; x++)
for(int y = 0; y < m; y++)
if(X[x][y])
for(int t = 0; t < 8; t++){
int nx = x + dx[t], ny = y + dy[t];
if(check(nx, ny) && X[nx][ny]){
puts("0");
return 0;
}
}
dfs(0, 0, 1);
print(ans);
浙公网安备 33010602011771号