《信息学奥赛一本通·提高篇》动态规划第4节—状态压缩类动态规划
【例 1】国王
#include <iostream>
#include <vector>
using namespace std;
typedef long long LL;//发现答案会溢出,所以开LL
const int N = 12;//最后输出答案的时候再解释
const int M = 1 << 10, K = 110;//所有状态的数量,国王的数量
int n, m;
vector<int> state;//所有合法状态
vector<int> head[M];//当前状态能转移到的所有合法状态
int cnt[M];//当前状态1(国王)的个数
LL f[N][K][M];//f[i][j][k]表示 前i层放置了j个国王,且第i层状态是k的方案
inline bool check(int st) {return !(st & (st >> 1));}
inline int count(int st)
{
int res = 0;
for (int i = 0; i < n; i ++) res += st >> i & 1;
return res;
}
int main()
{
cin >> n >> m;
//对所有状态预处理,把合法状态记录下来
for (int i = 0; i < (1<<n); i ++)
if (check(i))
{
state.push_back(i);
cnt[i] = count(i);
}
//枚举行的所有状态,看看哪些状态和哪些状态之间可以转移
for (int a : state)
for (int b : state)
//相同的位置不能同时有国王,并且上下两行的国王位置不能紧挨着
/*
0 1 0 0 国王的位置在2
0 0 0 1 国王的位置在4 这种状态就是合法的
0 1 0 0 0 1 0 0
0 1 0 0 0 0 1 0 这样的状态都是不合法的
*/
if(!(a & b) && check(a | b))
head[a].push_back(b);
f[0][0][0] = 1;//初始化
//因为最后要输出所有的方案数,采用一个小技巧,就是我们在枚举i的时候,枚举到n+1就可以了
//假设我们的棋盘是一个n+1 * n的一个棋盘,多了一行,但是最后一行什么都不放
//所以这里的n最大是11,下标是11的话,数组当然就要开12了,对应上面的N = 12
for (int i = 1; i <= n + 1; i ++)
for (int j = 0; j <= m; j ++)//国王数量 这里必须从0开始,因为有的行可以不放国王
for (int a : state) if (j >= cnt[a])//枚举一下第i行所有的状态,同时国王数量必须足够
for (int b : head[a])//枚举所有能转移到a的状态(上一行状态)
f[i][j][a] += f[i - 1][j - cnt[a]][b];
//输出第n + 1行,m个国王,第n + 1行一个国王都没有的情况数量
cout << f[n + 1][m][0];
return 0;
}
【例 2】牧场的安排
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
const int N = 14, M = 1 << 12, mod = 1e8;
int n, m;
int g[N];//记录每一行方格的状态,用二进制数表示
vector<int> state;//所有合法状态
vector<int> head[M];//当前状态能转移到的所有合法状态
int f[N][M];//f[i][j]表示前i层,且第i层状态为j的方案数量
//检查当前状态是否合法 任意一个种玉米的格子左边不能种玉米
bool check(int st) {return !(st & (st >> 1));}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++)
for (int j = 0; j < m; j ++)
{
int t; cin >> t;
g[i] += (!t << j);
//当前第i行的方格状态为 0 0 1 0 ,记录到g数组里为g[i] = 1 1 0 1
//之所以反着记录是因为待会要判断一下当前土地是否可以种植玉米
//直接一位一位比较土壤状况和种植情况太过麻烦
//将该行土壤状态的相反情况和该行的玉米种植情况进行&运算
//如果 = 0说明没有冲突,否则有冲突
}
//对所有行状态预处理,把合法状态记录下来
for (int st = 0; st < (1<<m); st ++)
if (check(st))
state.push_back(st);
//枚举所有行状态,看看哪些状态和哪些状态之间可以转移
for (int x : state)
for (int y : state)
if (!(x & y)) head[x].push_back(y);
f[0][0] = 1;//初始化
//因为最后要输出所有的方案数,采用一个小技巧,就是我们在枚举i的时候,枚举到n+1行就可以了
//假设我们的棋盘是一个n+1 * m的一个棋盘,多了一行,但是最后一行什么都不放
//所以这里的n最大是13,下标是13的话,数组当然就要开14了,对应上面的N = 14
for (int i = 1; i <= n + 1; i ++)
for (int j : state)//枚举一下第i行所有的状态
if (!(j & g[i]))//如果当前种玉米的状态和题目给出的土壤状态没有冲突
for (int k : head[j])//枚举能转移到j状态的所有状态(也就是枚举上一行的状态)
f[i][j] = (f[i][j] + f[i - 1][k]) % mod;
//输出第n + 1行状态为0的情况
cout << f[n + 1][0] << endl;
return 0;
}
涂抹果酱
#include <iostream>
#include <vector>
using namespace std;
const int N = 1e5 + 10, M = 310, mod = 1e6;
typedef long long LL;
int n, m, k;
int f[N][M];
//f[i][j]表示已经考虑完前i行,且第i行状态是j(同时所有状态的第1行都从题目所给的第k行的状态转移而来)的方案个数
int c[10] = {1};//c[i]表示3的i次方
vector<int> state, head[M];//记录所有合法状态 记录每个状态能转移到的状态
//取出x的第k位 等同于二进制下的 x >> k & 1 个位也就是最后一位为第0位
inline int get_k(int x, int k) {return x % c[k + 1] / c[k];}
//判断当前行状态为st时,该行状态是否合法
inline bool check1(int st)
{
for (int i = 0; i < m - 1; i ++)
if (get_k(st, i) == get_k(st, i + 1))//相邻区域的果酱不能一样
return false;
return true;
}
//判断相邻两行的状态x和y是否冲突
inline bool check2(int x, int y)
{
for (int i = 0; i < m; i ++)
if (get_k(x, i) == get_k(y, i))//相邻两行的同一位置要保证果酱不同
return false;
return true;
}
int main()
{
cin >> n >> m >> k;
int t = 0;//t用来记录第k行的状态
for (int i = 0; i < m; i ++)
{
int x; cin >> x;
t = t * 3 + x - 1;//将t转化成一个三进制数
}
for (int i = 1; i <= 5; i ++) c[i] = c[i - 1] * 3;//预处理3的i次方
for (int st = 0; st < c[m]; st ++)//枚举所有状态 等同于二进制中的 i < st < (1<<m)
if (check1(st))
state.push_back(st);
//和二进制状态压缩基本差不多
for (int a : state)
for (int b : state)
if (check2(a, b)) head[a].push_back(b);
f[0][t] = 1;//初始化 第一行的所有状态只能从t状态转移而来
int x = max(k - 1, n - k), y = min(k - 1, n - k);
for (int i = 1; i <= x; i ++)
for (int a : state)
for (int b : head[a])
f[i][a] = (f[i][a] + f[i - 1][b]) % mod;
int ans1 = 0, ans2 = 0;
for (int st : state) ans1 = (ans1 + f[x][st]) % mod, ans2 = (ans2 + f[y][st]) % mod;
cout << (LL)ans1 * ans2 % mod << endl;
return 0;
}
炮兵阵地
#include <iostream>
#include <vector>
using namespace std;
const int N = 110, M = 1 << 10;
int n, m;
int g[N], cnt[M];//地图 cnt[i]表示i状态下炮兵部队的数量
int f[2][M][M];//普通数组占空间太大,超过题目要求内存限制,改用滚动数组 滚动数组就是 &1 就行
vector<int> state, head[M];
//同一行的炮兵部队直接距离必须大于2
inline bool check(int st) {return !(st & st >> 1 || st & st >> 2);}
inline int count(int st)//当前行部署了多少炮兵部队
{
int res = 0;
while (st) res += st & 1, st >>= 1;
return res;
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++)
for (int j = 0; j < m; j ++)
{
char c; cin >> c;
g[i] += (c == 'H') << j;//不能放炮兵的位置记为1
}
for (int st = 0; st < (1<<m); st ++)
if (check(st))
{
state.push_back(st);
cnt[st] = count(st);
}
//找出能转移到当前状态的上一个状态
for (int cur_st : state)//枚举当前状态
for (int pre_st : state)//枚举上一行状态 0 1 0 0 1
if (!(cur_st & pre_st))//如果炮兵布置在相邻两行的同一列 比如1 0 0 0 1就不行
head[cur_st].push_back(pre_st);//说明可以转移,记录下来
for (int i = 1; i <= n + 2; i ++)//一样是小技巧,直接枚举到第n+2行,然后输出f[n+2&1][0][0]
for (int st : state)//枚举当前行状态
if (!(g[i] & st))//和题目给的地图没有冲突,(山地上不能够部署炮兵部队)
for (int p1 : head[st])//枚举上一行
for (int p2 : head[p1])//枚举上一行的上一行
//注意:这时还要再判断一下st和p2 这两行也不能有炮兵布置在同一个位置
if (!(st & p2))//滚动数组大概意思就是奇偶切换,慢慢悟吧
{
int &t = f[i & 1][st][p1];
t = max(t, f[i - 1 & 1][p1][p2] + cnt[st]);
//f[i - 1 & 1][p1][p2] + cnt[st])表示前i-1行炮兵数量+当前行st的炮兵数量
}
cout << f[n + 2 & 1][0][0];
return 0;
}
动物园
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 10010, M = 35;
int n, m;
int f[N][M], d[N][M];
//f[i][j]表示枚举到第i个围栏且[i,i+4]的围栏移走状态为j时的最多高兴人数
//d[i][j]表示[i,i+4]这个区间内的围栏移走情况为j时高兴的小朋友数量
int ans;
template <class T>
inline void read(T &res)
{
char ch; bool flag = false;
while ((ch = getchar()) < '0' || ch > '9')
if (ch == '-') flag = true;
res = (ch ^ 48);
while ((ch = getchar()) >= '0' && ch <= '9')
res = (res << 1) + (res << 3) + (ch ^ 48);
if (flag) res = ~res + 1;
}
int main()
{
read(n), read(m);
for (int i = 1; i <= m; i ++)
{
int a, b, c;
read(a), read(b), read(c);
int like = 0, fear = 0;//当前小朋友可以看到的围栏中对动物的喜欢和害怕的情况
for (int j = 1; j <= b; j ++)
{
int k; read(k);//k表示当前围栏是从a围栏开始的第几个围栏
k = (k - a + n) % n;//取模是为了处理环
fear |= 1 << k;
}
for (int j = 1; j <= c; j ++)
{
int k; read(k);
k = (k - a + n) % n;
like |= 1 << k;
}
for (int st = 0; st < 32; st ++)//遍历从a围栏开始移走围栏的所有状态
if ((st & fear) || (~st & like))
d[a][st] ++;
}
//起始的[1,5]区间的围栏状态是由[0,4]区间转移而来的,所以要枚举一下[0,4]的状态
for (int i = 0; i < 32; i ++)//遍历[0,4]区间围栏的所有移走状态
{
memset(f, -0x3f, sizeof f);//初始化
f[0][i] = 0;//初始化
for (int j = 1; j <= n; j ++)//枚举所有起点
for (int st = 0; st < 32; st ++)//枚举[j,j+4]该区间内围栏的状态
f[j][st] = max(f[j-1][(st&15)<<1], f[j-1][(st&15)<<1|1]) + d[j][st];
//f[n][i]即为这一轮当中高兴的小朋友的数量
ans = max(ans, f[n][i]);
}
printf("%d\n", ans);
return 0;
}