牵扯到选不选,而且选择达到的目标给的范围很小的时候,多半可以压缩状态。
AcWing 1243:
https://www.acwing.com/problem/content/description/1245/
- 为啥用dp:最优子结构:所有口味都品尝到(1 1 1 1 1)的最小包数必定由某个状态(0 1 0 1 1) + 某一个可以把剩下的0填满的包转移过来。
- 为什么要状态压缩:1.每个口味就两种状态 选到和没选到 -> 二进制 2.数据范围很小(口味一共20个,220)3.口味的组合可能很多,错综复杂。
- 这里不需要关心选取的顺序问题,每个最优的合法状态肯定是由一包包糖果凑出来的,枚举的时候每个状态肯定都是最优的。
- 为什么不需要将第几包也计入状态: 1.当前这包和前面的那些包并没有直接转移关系。(像后面学生座位的那道题就有关系)2. 并且如果第i包从第 i − 1 i-1 i−1包的状态转移的话,还要先将第 i − 1 i-1 i−1包已经优化的状态全部复制过去。(因为 d p ( i , s t ) dp(i,st) dp(i,st)全是初始值,而很多 s t st st已经优化过了)
- 1.状态定义: d p ( s t ) dp(st) dp(st)为(口味)状态为 s t st st(二进制)的集合,属性是最小包数。
- 2.状态转移: d p ( n s t ) = m i n ( d p ( s t ) + 1 , d p ( n s t ) ) ( n s t = s t ∣ t ) dp(nst)=min(dp(st)+1, dp(nst))(nst=st|t) dp(nst)=min(dp(st)+1,dp(nst))(nst=st∣t) 枚举每一包糖,然后枚举所有状态,用合法的状态(选择前面几包的时候能够获得的状态)与上当前这包所能填补的空位去获得新的状态,然后更新最小包数。对于集合的划分,就是到第有没有用当前这一包。其实这里不用的话,也没有去更新。用了的话,就去取最小值。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<vector>
#include<string>
#include<map>
#include<set>
#include<unordered_map>
#include<unordered_set>
int n, m, k;
int can[100][20], dp[1 << 20];
int main(void){
cin >> n >> m >> k;
memset(can, 0, sizeof(can));
memset(dp, -1, sizeof(dp));
for (int i = 0; i < n; ++i)
for (int j = 0; j < k; ++j){
cin >> can[i][j];
can[i][j]--;
}
int lim = 1 << m;
dp[0] = 0;
for (int i = 0; i < n; ++i){
int t = 0;
for (int j = 0; j < k; ++j) t |= (1 << can[i][j]);
for (int st = 0; st < lim; ++st){
if (dp[st] == -1) continue;
int nst = st | t;
if (dp[nst] == -1 || dp[nst] > dp[st] + 1) dp[nst] = dp[st] + 1;
}
}
cout << dp[lim - 1];
return 0;
}
LeetCode 1125:
题目链接: https://leetcode-cn.com/problems/smallest-sufficient-team/
- 这题和上体基本没啥区别,除了输出的时候要求选的是哪些人。
- 那么只需要用key为状态,val为返回数组的字典即可。每次状态转移的时候,去更新当前nst映射的那个最佳人员(数组)即可。
class Solution {
private:
unordered_map<string, int> rec;
int dp[1 << 16 + 5];
unordered_map<int, vector<int>> ans;
public:
vector<int> smallestSufficientTeam(vector<string>& rS, vector<vector<string>>& peo) {
int n = rS.size();
for (int i = 0; i < n; ++i) rec[rS[i]] = i;
memset(dp, -1, sizeof (dp));
dp[0] = 0;
int lim = 1 << n;
for (int i = 0, len = peo.size(); i < len; ++i){
const auto & arr = peo[i];
int t = 0;
for (const auto & skill: arr) t |= (1 << rec[skill]);
for (int st = 0; st < lim; ++st){
if (dp[st] == -1) continue;
int nst = st | t;
if (dp[nst] == -1 || dp[nst] > dp[st] + 1){
dp[nst] = dp[st] + 1;
ans[nst] = ans[st];
ans[nst].push_back(i);
}
}
}
return ans[lim - 1];
}
};
LeetCode 1349
题目链接:https://leetcode-cn.com/problems/maximum-students-taking-exam/submissions/
- 为什么使用动态规划:该问题有最优子结构、子问题 ,前i行必定由前i-1行最多的 + 第i行的座位数(只不过第i行要和第i-1行不冲突)转移过来。
- 为什么压状dp:首先因为第i行必定和第i-1行有关,行数必定在状态里,那么第i-1行的座位分布必定也需要在状态里,不然无法转移。因为这里座位数量很少,我们可以选择压缩成二进制。 不然的话,我们需要一个数组去判断每个位置是否有人。
- 1.状态定义: d p ( i , s t ) dp(i, st) dp(i,st) 第i行,状态为st的时候,前i行的座位集合,属性为最多座位数。
- 2.状态转移: d p ( i , s t ) = d p ( i − 1 , l s t ) + b i t s c o u n t ( s t ) dp(i,st)=dp(i-1,lst)+bitscount(st) dp(i,st)=dp(i−1,lst)+bitscount(st) 当前这个状态,必定由上一行(与当前行符合条件的)状态 + 现在这行的座位数 转移过来。所以,我们首先枚举每一行,再枚举这行的每个状态,去掉不合法状态(坏掉位置坐人、相邻坐人),再去枚举上一行的状态,略去不合法状态(上一行本来就不合法、上一行和这一行冲突),然后转移。
- 3.答案统计:由最后一行的各个状态中,取最大值。
class Solution {
private:
static const int N = 10;
int dp[N][1 << N];
int bitsCount(int t){
int cnt = 0;
while (t){
if (t & 1) cnt++;
t >>= 1;
}
return cnt;
}
public:
int maxStudents(vector<vector<char>>& seats) {
memset(dp, 0, sizeof(dp));
int n = seats.size(), m = seats[0].size();
for (int i = 1; i <= n; ++i){
for (int j = 0; j < (1 << m); ++j){ // 枚举当前这一行的状态
//当前状态 左右不能同时坐人 且要坐在合法位置上
bool flag = true;
for (int k = 0; k < m; ++k){ // 枚举座位,坏掉的座位不能坐人
if (seats[i - 1][k] == '#' && (1 & (j >> k)) ) {flag = false; break;}
}
if (!flag || (j & (j << 1)) || (j & (j >> 1))){
dp[i][j] = -1; continue;
}
for (int lst = 0; lst < (1 << m); ++lst){
if (dp[i - 1][lst] == -1) continue;
if ((lst >> 1) & j || (lst << 1) & j) continue;
dp[i][j] = max(dp[i][j], dp[i - 1][lst] + bitsCount(j));
}
}
}
int ans = 0;
for (int st = 0; st < (1 << m); ++st) ans = max(ans, dp[n][st]);
return ans;
}
};
LeetCode 1434
题目链接:https://leetcode-cn.com/problems/number-of-ways-to-wear-different-hats-to-each-other/
- 为什么用动态规划:最优子结构、重叠子问题:所有人都戴上帽子的方案数可以由(第i个人没戴帽子,其余人戴了帽子 )的方案数 + (第j个人没带,其余人戴了)的方案数(第i个人和第j个人都喜欢第h顶帽子)
- 为什么状态压缩:这题和前面很相似:选不选,并且都选择达到的目标数据很小(每个人都戴帽子的状态),所以压缩状态。这题帽子也要记录在状态里(但是可以用滚动数组压成一维):按照之前分析的 (所有人都戴上帽子的方案数可以由(第i个人没戴帽子,其余人戴了帽子 )的方案数 + (第j个人没带,其余人戴了)的方案数(第i个人和第j个人都喜欢第h顶帽子)),第h顶的某个状态要由前h-1顶的那些状态转移过来。
- 预处理:因为对于每顶帽子来说,我需要看它能够分给哪些人。所以新建立一个字典:key为帽子,val为可以分给的人数组。
- 状态定义: d p ( i , s t ) dp(i,st) dp(i,st)前i顶帽子,状态为st的方案集合,属性为数量。
- 状态转移:
d
p
(
i
,
s
t
)
=
d
p
(
i
−
1
,
s
t
)
+
d
p
(
i
−
1
,
l
s
t
)
(
l
s
t
=
s
t
(
1
<
<
p
)
)
dp(i,st)=dp(i-1,st)+dp(i-1,lst)(lst=st^(1<<p))
dp(i,st)=dp(i−1,st)+dp(i−1,lst)(lst=st(1<<p)) 枚举每顶帽子,枚举每个状态。集合的划分就由帽子每个人、给了哪些人来划分。 对于当前帽子,可以选择不给任何一个人,直接加上
d
p
(
i
−
1
,
s
t
)
dp(i-1,st)
dp(i−1,st);也可以选择给一个帽子可以发的人(当前状态那个人是需要帽子的),然后当前
st^(1<<p)
获得那个人还没戴帽子的状态,然后转移过来。
class Solution {
private:
static const int MOD = 1e9 + 7;
unordered_map<int, vector<int>> rec;
public:
int numberWays(vector<vector<int>>& hats) {
// 10个人
// dp(i, j) 前i顶帽子 状态为j 的方案数
int n = hats.size();
int dp[41][1 << 10];
memset(dp, 0, sizeof(dp));
dp[0][0] = 1;
// 第i顶帽子可以送给哪些人
for (int i = 0; i < n; ++i){
for (int j = 0, lim = hats[i].size(); j < lim; ++j){
int h = hats[i][j];
rec[h].push_back(i);
}
}
for (int h = 1; h <= 40; ++h){
for (int s = 0; s < (1 << n); ++s){
dp[h][s] = (dp[h - 1][s] + dp[h][s]) % MOD;
for (const int & p: rec[h]){
// 当前需要帽子
if (s & (1 << p)){
// 找到之前没戴帽子的状态
int ns = s ^ (1 << p);
dp[h][s] = (dp[h][s] + dp[h - 1][ns]) % MOD;
}
}
}
}
return dp[40][(1 << n) - 1];
}