算法竞赛进阶指南 0x51 状态压缩DP
先补充一波位运算基础知识啊
最短Hamilton路径
这题是不是很眼熟?没错,这就是0x01那一节里面的原题
0x01节里就说了最好等学到状态压缩DP在回来整这道题,这不终于学到状态压缩DP了吗,怎么能不复(学)习一下呢
#include <iostream>
#include <cstring>
using namespace std;
const int N = 20, M = 1 << 20;
int n, w[N][N];
int f[M][N];//f[i][j]表示从0出发当前走过的点集为i,终点为j的最短哈密顿路径
int main()
{
cin >> n;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < n; j ++)
cin >> w[i][j];
memset(f, 0x3f, sizeof f);
f[1][0] = 0;//初始化 起点是0,所有状态为1,终点为0的最短哈密顿路径是0
//枚举所有state,且state必须包含起点1
for (int st = 0; st < (1<<n); st ++) if (st & 1)
for (int i = 0; i < n; i ++) if (st >> i & 1)//枚举所有当前state到过的点作为终点
//枚举以j作为终点的情况,j必须保证被经过,且不等于i
for (int j = 0; j < n; j ++) if ((st >> j & 1) && j != i)
//状态为st终点为i的情况由状态为st-(1<<i)终点为j的情况转移而来
f[st][i] = min(f[st][i], f[st - (1<<i)][j] + w[j][i]);
cout << f[(1<<n) - 1][n - 1];
return 0;
}
老活新的整了属于是
Mondriaan’s Dream
#include <iostream>
#include <cstring>
using namespace std;
const int N = 12, M = 1 << N;
typedef long long LL;//爆int了
int n, m;
bool st[M];
LL f[N][M];
int main()
{
while (cin >> n >> m && (n || m))
{
memset(st, 1, sizeof st);//一开始全部标记为合法状态
//先预处理出所有列当中合法的状态
for (int i = 0; i < 1 << n; i ++)//枚举所有可能,每一列的情况都在这 1<<n 种情况之中
{
int cnt = 0;
for (int j = 0; j < n; j ++)//循环列中的每一行
{
if (i >> j & 1)//如果该位置有格子捅出来了要判断一下之前有多少连续的没有捅出来的格子
{
if (cnt & 1) st[i] = false;
// cnt 为当前已经存在多少个连续的0,cnt为奇数的话 cnt&1为真,即当前状态不合法
cnt = 0;
}
else cnt ++;
}
if (cnt & 1) st[i] = false; // 扫完后要判断一下最后一段有多少个连续的0
}
memset(f, 0, sizeof f);
f[0][0] = 1;
for (int i = 1; i <= m; i ++)//枚举所有列
for (int j = 0; j < 1 << n; j ++)//枚举当前列所有状态
for (int k = 0; k < 1 << n; k ++)//枚举上一列所有状态
if ((j & k) == 0 && (st[j | k]))
//j & k == 0 表示i列和i - 1列没有同一行同时捅出来
//第i列原本就有横着放的格子,再加上第i-1列横着放的,第i列的最终状态应该是j|k
//所以j|k也必须得是合法状态
f[i][j] += f[i - 1][k];
cout << f[m][0] << endl;
}
return 0;
}
[NOI2001]炮兵阵地
预处理版
#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 <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 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 : state)//枚举上一行
for(int p2 : state)//枚举上一行的上一行
if(!((st & p1) || (st & p2) || (p1 & p2)))//每两行之间都不能冲突
{
int &t = f[i & 1][st][p1];
t = max(t, f[i - 1 & 1][p1][p2] + cnt[st]);
}
cout << f[n + 2 & 1][0][0];
return 0;
}
还是建议预处理
宝藏
#include <iostream>
#include <cstring>
using namespace std;
const int N = 15, M = 1 << N, K = 1010;
const int INF = 0x3f3f3f3f;
int n, m;
int g[N][N];
int ne[M];//ne[i]表示状态i扩展一层后能得到的最大的下一层状态
int f[M][K];
int get_cost(int cur, int pre)//计算pre到cur所需的最短路径长度
{
//ne[pre]表示pre能转移到的最大的下一层状态,如果最大状态都没有包含cur说明pre无法转移到cur
if ((ne[pre] & cur) != cur) return -1;
int k = pre ^ cur, cost = 0;//k表示从pre到cur需要新增的点
for (int i = 0; i < n; i ++)
if (k >> i & 1)//遍历k的每一个点
{
int len = INF;
for (int j = 0; j < n; j ++)//寻找pre到该点的最短路径
if (pre >> j & 1)
len = min(len, g[j][i]);
cost += len;
}
return cost;
}
int main()
{
cin >> n >> m;
memset(g, 0x3f, sizeof g);//建图初始化
for (int i = 0; i < n; i ++) g[i][i] = 0;//这一行不写居然会WA
while (m --)
{
int a, b, c;
cin >> a >> b >> c;
a --, b --;//点的编号从0开始
g[a][b] = g[b][a] = min(g[a][b], c);
}
//预处理所有状态扩展一层后能够得到的最大的下一层状态
for (int st = 1; st < (1<<n); st ++)
for (int i = 0; i < n; i ++)
if (st >> i & 1)//遍历当前状态所有点
for (int j = 0; j < n; j ++)
if (g[i][j] != INF)//遍历i号点所以能连通的点
ne[st] |= 1 << j;
memset(f, 0x3f, sizeof f);
for (int i = 0; i < n; i ++) f[1 << i][0] = 0;//每次可以白嫖一个宝藏点作为起点
for (int cur = 1, cost; cur < (1<<n); cur ++)//枚举所有状态
for (int pre = (cur - 1) & cur; pre; pre = (pre - 1) & cur)//枚举cur所有子集(上一层)
if (~(cost = get_cost(cur, pre)))//要保证cost不为-1
for (int k = 1; k < n; k ++)//枚举层数
f[cur][k] = min(f[cur][k], f[pre][k - 1] + cost * k);
int res = INF;//在所有层当中找最小的方案
for (int k = 0; k < n; k ++) res = min(res, f[(1<<n) - 1][k]);
cout << res;
return 0;
}
学完这一章后,什么位运算不会啊,想不出来啊,直接轻松给他拿捏