什么是子集枚举?
就是在状态压缩后,枚举该状态的子状态。
做法
1. 一个 \(4^n\) 做法,直接枚举所有情况,并判断两个集合 \(S\) 和 \(T\) 中 \(T \in S\)。
cpp
for (int s = 0; s < (1 << n); s++) {
for (int t = 0; t < (1 << n); t++) {
if ((s & t) == t) {
// t是s的子集
}
}
}
2. \(3^n\) 做法,借助位运算降低复杂度。
cpp
for (int s = 0; s < (1 << n); s++) {
for (int t = s; t; t = (t - 1)&s) {
// t是s的子集
}
}
复杂度证明
假设集合大小为 \(n\)。
对于一个特定的 \(mask\),如果它包含 \(k\) 个 \(1\),那么它有 \(2^k\) 个子集。
\(mask\) 中包含 \(k\) 个 \(1\) 的情况共有 \(C_n^k\)(即组合数 {n \\choose k} )种。
因此总的操作次数为:
\[\sum_{k=0}^n {n \choose k} 2 ^ k \]
由二项式定理\((x + y) ^ n = \sum {n \choose k} x^k y^{n-k}\)可得:
代入 \(x = 2, y = 1\) 得 $$ \sum_{k=0}^n {n \choose k} 2^k 1^{n-k} = (2 + 1) ^ n = 3^n $$
简单例题:Cows in a Skyscraper G
做法
两种做法。
- 搜索
用 \(sum\) 数组存下每个电梯所装的奶牛的重量。
dfs 中传入两个参数:\((r, cnt)\)
分别表示:当前奶牛和电梯数量。
现在就简单了,在每一次 dfs 中,枚举每一个电梯。
- 如果能装进此电梯,就装进去,并往下。
- 枚举完了再 dfs 一次,表示增加一个电梯。
最后记录最小值即可。
有几个剪枝方案。
- 如果当前的电梯数量已经大于了最小的数量,那么就不枚举了。
- 在 dfs 前,将奶牛的重量从大到小的排序,这样就减少了可行方案。
戳我看代码喵~
int n, w;
lint val[N];
lint sum[N];
int ans = 1e9;
bool vis[N];
void dfs(int r, int cnt) {
if (cnt > ans) return ;
if (r == n) {
ans = min(ans, cnt);
return ;
}
for (int i = 0; i < cnt; i++) if (sum[i] + val[r] <= w) {
sum[i] += val[r];
dfs(r + 1, cnt);
sum[i] -= val[r];
}
sum[cnt] = val[r];
dfs(r + 1, cnt + 1);
sum[cnt] = 0;
}
int main() {
n = re, w = re;
for (int i = 0; i < n; i++) val[i] = re;
sort(val, val + n, greater<int>());
dfs(0, 1);
wr(ans), endl;
}
- DP做法
这种做法还要分两种做法。
- O(3\^n) 做法
\(dp_s\) 表示状态为 \(s\) 时,需要的最少电梯次数。
转移式还是很简单
\[dp_s = min(dp_{s \oplus t}),{t \subseteq s} \]
戳我喵~
int n, w;
lint val[30], W[(1 << 18) + 10];
lint dp[(1 << 18) + 10];
signed main() {
IAKIOI;
n = re, w = re;
for (int i = 0; i < n; i++) val[i] = re;
memset(dp, 0x3f, sizeof dp);
dp[0]=0;
for (int s = 1; s < (1 << n); s++) for (int i = 0; i < n; i++) if ((s >> i) & 1) W[s] += val[i];
for (int s = 1; s < (1 << n); s++) for (int t = s; t; t = (t - 1) & s) if (W[t] <= w) dp[s] = min(dp[s], dp[s ^ t] + 1);
wr(dp[(1 << n) - 1]), endl;
}
- \(O(2 ^ n \times n ^ 2)\) 做法
dp_{i, s} :表示当前已经开启了 \(i\) 架电梯, 且当前已下楼奶牛的状态为 \(s\) 的情况下,最后一架电梯当前的载重量。
- 当前电梯还能装下,即不用新开电梯
那么
\[dp_{i, s | (1 << k)} = min(dp_{i, s} + val_k) \]
- 当前电梯不能装下,即需要新开电梯
那么
\[dp_{i + 1, s | (1 << k)} = min(val_k) \]
戳我喵~
int n, w;
int val[30];
int dp[30][(1 << 21) + 10];
signed main() {
IAKIOI;
n = re, w = re;
for (int i = 0; i < n; i++) val[i] = re;
memset(dp, INF, sizeof dp);
for (int i = 0; i < n; i++) dp[i + 1][1 << i] = val[i];
for (int s = 1; s < (1 << n); s++) for (int i = 1; i <= n; i++) if (dp[i][s] <= w) for (int k = 0; k < n; k++) if (!((s >> k) & 1)) {
if (dp[i][s] + val[k] <= w) dp[i][s | (1 << k)] = min(dp[i][s | (1 << k)], dp[i][s] + val[k]);
else dp[i + 1][s | (1 << k)] = min(dp[i + 1][s | (1 << k)], val[k]);
}
for (int i = 1; i <= n; i++) if (dp[i][(1 << n) - 1] != INF) {
wr(i), endl;
return 0;
}
}