子集枚举
给定一个包含n个元素的数组a,子集枚举就是枚举数组a的所有子集,每个元素有选和不选两种状态,故可以用二进制来表示
故我们可以用一个n位二进制数mask,若第i位为0,则表示不选第i个元素,若第i位为1,则表示选第i个元素
模板
1
2
3
4
5
6
7
for (int mask = 0; mask < (1 << n); mask++) {
for (int i = 0; i < n; i++) {
if (mask & (1 << i)) {
// 选中了 a[i]
}
}
}其中,mask表示当前子集的二进制状态,从0到\(2^n-1\)
则第三行表示第i个元素是否被选
例
小红拿到了一个长为n的数组 \(a=\{a_1,a_2,\cdots,a_n\}\),她要从中取若干个元素,使得这些元素的和恰好为 k。 现在小红想知道,有多少种不同的合法取数方案,请你帮帮她。
Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void solve() {
ll n;
ll k;
cin >> n >> k;
vector<ll> arr(n+1);
for (int i = 1; i<=n; i++)
{
cin >> arr[i];
}
ll cnt = 0;
for (int i = 1; i<(1ll<<n); i++)
{
ll sum = 0;
for (int j = 1; j<=n; j++)
{
if(i&(1ll<<(j-1)))
{
sum+=arr[j];
if (sum>k) break;
}
}
if (sum==k) cnt++;
}
cout << cnt << endl;
}状压DP
状态转移时,数组形式的可变参数不方便修改(如数组中某个元素是否被使用),我们可以用整数位信息来表示某个样本能否还能使用,并利用这个信息进行尝试,进而进行动态规划
若有k个样本,则有\(2^k\)个状态
注意,状压dp能解决的问题一般数量都不大,在20个以内
例1-我能赢吗
状压dp的递归版本,通过传递st来表示选或不选
题中有两个可变参数:st与rest,但只需要对st作缓存,rest是被st决定的
即只需要关注最关键的可变参数,被决定的不需要管
dp表的含义:
dp[st]==0代表没算过dp[st]==1代表算过,答案是truedp[st]==-1代表算过,答案是false
递归函数f的含义:
在数字范围1-n的情况下,当前先手面对st的数字状态,在累加和还剩rest的情况下返回先手能不能赢
能赢返回true,不能返回false
时间复杂度:\(O(n*2^n)\)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bool canIWin(int n, int m) {
if (m==0) return true;
if (n*(n+1)/2<m) return false;
vector<int> dp((1<<(n+1)));
return f(n, (1<<(n+1))-1, m, dp);
}
bool f(int n, int st, int rest, vector<int> &dp)
{
if (rest<=0) return false;
if (dp[st]!=0) return dp[st]==1;
bool flag = 0;
for (int i = 1; i<=n; i++)
{
if ((st&(1<<i))&&!f(n, (st^(1<<i)), rest-i, dp))
{
flag = 1;
break;
}
}
dp[st] = flag?1:-1;
return flag;
}例2-火柴拼正方形
同样只需要对st做缓存,cur和re都是随着st变化的
f函数里,每次选择一条边,提前算好正方形每条边的长度,用cur计算目前累计的长度
若cur加上下一条边刚好是需要的长度,就重新计算下一条边,re(需要的边数)-1
否则就继续累加当前边数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
bool makesquare(vector<int>& matchsticks) {
int sum = 0;
for (auto it: matchsticks)
{
sum+=it;
}
if (sum%4) return false;
int n = matchsticks.size();
vector<int> dp((1<<n)+1);
return f(matchsticks, sum/4, (1<<n)-1, 0, 4, dp);
}
bool f(vector<int> nums, int len, int st, int cur, int re, vector<int> &dp)
{
if (st==0)
{
return re==0;
}
if (dp[st])
{
return dp[st]==1;
}
bool flag = 0;
for (int i = 0; i<nums.size(); i++)
{
if ((st&(1<<i))&&cur+nums[i]<=len)
{
if (cur+nums[i]==len)
{
flag = f(nums, len, st^(1<<i), 0, re-1, dp);
}
else
{
flag = f(nums, len, st^(1<<i), cur+nums[i], re, dp);
}
if (flag) break;
}
}
dp[st] = flag?1:-1;
return flag;
}例3-TSP问题
https://www.luogu.com.cn/problem/P1171
这里和上面的想法类似,注意此时走过是1,未走过是0
f中st代表状态,i代表目前在哪个村
f从1开始,表示0号村已经被走过,在f函数里,若st全置一,证明都走到了一遍,此时需要返回0号村,因此return到零号村的代价
dp状态返回同理
在走村时,遍历每一个村,若某位为0即未走过,就走,记录代价最小值
注意,洛谷的#9存在卡常,这里改成静态数组才过
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int n;
int g[N][N];
int dp[1<<N][N];
int f(int st, int i)
{
if (st==(1<<n)-1)
{
return g[i][0];
}
if (dp[st][i]!=-1)
{
return dp[st][i];
}
int ans = INT_MAX;
for (int j = 0; j<n; j++)
{
if ((st&(1<<j))==0)
{
ans = min(ans, g[i][j]+f(st|(1<<j), j));
}
}
dp[st][i] = ans;
return ans;
}
void solve() {
cin >> n;
memset(dp, -1, sizeof(dp));
for (int i = 0; i<n; i++)
{
for (int j = 0; j<n; j++)
{
cin >> g[i][j];
}
}
cout << f(1,0);
}例4-每个人戴不同帽子的方案数
注意选取什么元素当作状态
这个题里,人数最多10个,可以将人数作为状态使用
对每种出现的颜色,看他们能不能满足每个人(将他们编号),对应的是二进制位的不同位置
分为两种可能性:
- i颜色的帽子不分配给任何人,直接跳到下一层
- i颜色的帽子分配给0~n-1可能的每一个人
优化:可以不用枚举每一个人,而是从当前帽子中依次提取出能满足的人,即提取出二进制状态中最右侧的1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
int numberWays(vector<vector<int>>& hats) {
int maxn = -1;
for (auto lt: hats)
{
for (auto it: lt)
{
maxn = max(maxn, it);
}
}
vector<int> ph(maxn+1);
int n = hats.size();
for (int i = 0; i<n; i++)
{
for (auto it: hats[i])
{
ph[it]|=(1<<i);
}
}
vector<vector<int>> dp(maxn+1, vector<int>((1<<n)+1, -1));
return f(ph, n, maxn, 1, 0, dp);
}
int f(vector<int> &ph, int n, int m, int i, int st, vector<vector<int>>&dp)
{
if (st==(1<<n)-1)
{
return 1;
}
if (i==m+1)
{
return 0;
}
if(dp[i][st]!=-1)
{
return dp[i][st];
}
int ans = f(ph,n, m, i+1, st, dp);
int cur = ph[i];
for (int j = 0; j<n; j++)
{
if ((cur&(1<<j))&&(st&(1<<j))==0)
{
ans = (ans+f(ph,n, m, i+1, st|(1<<j), dp))%mod;
}
}
dp[i][st] = ans;
return ans;
}例5-分配重复整数
用到的技巧:枚举st的所有子集状态
如:st目前的状态为1001100,它有多少子集状态
使用循环枚举:
1
for (int i = st, j>0; j = (j-1)&st) {...}通过枚举quantity的每个子集,来记录每种情况所需要的重复数字个数
1
2
3
4
5
6
7
8
9
10
11
int m = quantity.size();
vector<int> sum((1<<m));
for (int i = 0; i<quantity.size(); i++)
{
int v = quantity[i];
int h = 1<<i;
for (int j = 0; j<h; j++)
{
sum[h|j] = sum[j]+v;
}
}我们需要看当前需要的数字数,在当前状态的某一个子集中是否满足,而不一定是该状态必须满足当前数字数,因为某个状态可以满足多个人需要的数字数,故需要枚举状态的子集
1
2
3
4
5
6
7
8
9
10
11
bool flag = 0;
int c = cnt[i];
for (int j = st; j>0; j = (j-1)&st)
{
if (sum[j]<=c&&f(cnt,sum, st^j, i+1, dp))
{
flag = 1;
break;
}
}
if (!flag) flag = f(cnt, sum , st, i+1, dp);Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
bool canDistribute(vector<int>& nums, vector<int>& quantity) {
sort(nums.begin(), nums.end());
int n = 1;
for (int i = 1; i<nums.size(); i++)
{
if (nums[i-1]!=nums[i]) n++;
}
vector<int> cnt(n,0);
int c = 1;
for (int i = 1, j = 0; i<nums.size(); i++)
{
if (nums[i-1]!=nums[i])
{
cnt[j++] = c;
c = 1;
}
else
{
c++;
}
}
cnt[n-1] = c;
int m = quantity.size();
vector<int> sum((1<<m));
for (int i = 0; i<quantity.size(); i++)
{
int v = quantity[i];
int h = 1<<i;
for (int j = 0; j<h; j++)
{
sum[h|j] = sum[j]+v;
}
}
vector<vector<int>> dp(n, vector<int>((1<<m)));
return f(cnt, sum, (1<<m)-1, 0, dp);
}
bool f(vector<int>& cnt, vector<int>& sum, int st, int i, vector<vector<int>>& dp)
{
if (st==0) return true;
if (cnt.size()==i) return false;
if (dp[i][st]) return dp[i][st] == 1;
bool flag = 0;
int c = cnt[i];
for (int j = st; j>0; j = (j-1)&st)
{
if (sum[j]<=c&&f(cnt,sum, st^j, i+1, dp))
{
flag = 1;
break;
}
}
if (!flag) flag = f(cnt, sum , st, i+1, dp);
dp[i][st] = flag?1:-1;
return flag;
}例6-小红的矩阵修改
https://www.nowcoder.com/practice/e556fd4852954529aec85940801200a1
三进制状态压缩
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
void solve() {
int n, m;
cin >> n >> m;
map<char, int> mp = {{'r',0},{'e',1},{'d',2}};
vector<string> g(n);
for (int i = 0; i<n; i++)
{
cin >> g[i];
}
ll mask = pow(3,n);
vector<ll> ok;
auto cost = [&](int fr, int to)
{
int c = 0;
for (int i = 0; i<n; i++)
{
if(mp[g[i][fr]]!=to%3) c++;
to/=3;
}
return c;
};
for (int i = 0; i<mask; i++)
{
bool flag = true;
int temp = i;
int last = -1;
for (int j = 0; j<n; j++)
{
int cur = temp%3;
if (cur==last)
{
flag = false;
break;
}
last = cur;
temp/=3;
}
if (flag) ok.push_back(i);
}
vector<vector<ll>> dp(m+1, vector<ll>(mask, 0x3f3f3f3f));
for (auto c: ok)
{
dp[0][c] = cost(0, c);
}
for (int i = 1; i<m; i++)
{
for (auto cur: ok)
{
int c = cost(i,cur);
for (auto prv:ok)
{
bool flag = true;
int tcur = cur, tprv = prv;
for (int i = 0; i<n; i++)
{
if (tcur%3==tprv%3)
{
flag = false;
break;
}
tcur/=3, tprv/=3;
}
if (flag)
{
dp[i][cur] = min(dp[i][cur], dp[i-1][prv]+c);
}
}
}
}
ll ans = 0x3f3f3f3f;
for (auto it: ok)
{
ans = min(ans, dp[m-1][it]);
}
cout << ans << endl;
}
int main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int tomorin=1;
//cin >> tomorin;
while (tomorin--) solve();
return 0;
}