状压 DP 是动态规划的一种,通过将状态压缩为整数来达到优化转移的目的。
例一:互不侵犯
在 N×N 的棋盘里面放 K 个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共 8 个格子。
输入格式
只有一行,包含两个数 N,K。
输出格式
所得的方案数
输入输出样例对于全部数据,1≤N≤9,0≤K≤N×N。
思路:用二进制数模拟放置的状态,如100010是第一个和第五个有放。然后考虑可行性,先考虑同一行,不能有相邻的,可以通过位运算模拟。再考虑相邻两行,通过‘|’运算符check。接着进行dp,f[i][j][k]表示前i行放置了j个国王此时第i行状态为k,然后用f[i][j][k]加上i-1行时的全部情况,即
f[i][j][k]+=f[i-1][j-count(k)//减去那一行的国王数][m];
代码:
#include<iostream>
#include<vector>
using namespace std;
int n, k;
long long f[2][110][1 << 10];
vector<int>p;
vector<int>q[1 << 10];
bool check(int x)
{
for (int i = 0; i < n; i++)
{
if ((x >> i & 1) && (x >> (i + 1) & 1))
{
return false;
}
}
return true;
}
int count(int x)
{
int res = 0;
for (int i = 0; i < n; i++)
{
res += x >> i & 1;
}
return res;
}
int main()
{
cin >> n >> k;
for (int i = 0; i < 1 << n; i++)
{
if (check(i))
{
p.push_back(i);
}
}
for (int i = 0; i < p.size(); i++)
{
for (int j = 0; j < p.size(); j++)
{
if ((p[i] & p[j]) == 0 && (check(p[i] | p[j])))
{
q[i].push_back(j);
}
}
}
f[0][0][0] = 1;
for (int i = 1; i <= n + 1; i++)
{
for (int j = 0; j <= k; j++)
{
for (int r = 0; r < p.size(); r++)
{
f[i & 1][j][p[r]] = 0;
int cnt = count(p[r]);
if(j>=cnt)
{
for (int l : q[r])
{
f[i & 1][j][p[r]] += f[(i - 1) & 1][j - cnt][p[l]];
}
}
}
}
}
cout << f[(n + 1) & 1][k][0];
return 0;
}
例二:吃奶酪
房间里放着 n 块奶酪。一只小老鼠要把它们都吃掉,问至少要跑多少距离?老鼠一开始在 (0,0) 点处。
输入格式
第一行有一个整数,表示奶酪的数量 n。
第 2 到第 (n+1) 行,每行两个实数,第 (i+1) 行的实数分别表示第 i 块奶酪的横纵坐标 xi,yi。
输出格式
输出一行一个实数,表示要跑的最少距离,保留 2 位小数。
思路:f[i][j]
表示当前位于点 i
,已经访问过的点的状态为 j
时的最短路径长度。这里使用二进制状态压缩来表示哪些点已经被访问过。
f[i][k]=min(f[j][k-(1<<(i-1))]+dis(i,j),f[i][k]);通过这个式子不断更新,最后f[][1<<(i-1)-1]的最大值即为答案.
代码:
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstdio>
using namespace std;
int n;
double x[20];
double y[20];
double f[17][1 << 17];
double dis(int a, int b)
{
return sqrt(pow(x[a] - x[b], 2) + pow(y[a] - y[b], 2));
}
int main()
{
cin >> n;
x[0] = 0;
y[0] = 0;
for (int i = 0; i <= n; i++)
{
for (int j = 1; j < (1 << n); j++)
{
f[i][j] = 99999999;
}
}
for (int i = 1; i <= n; i++)
{
cin >> x[i] >> y[i];
}
for (int i = 1; i <= n; i++)
{
f[i][1 << (i - 1)] = dis(0, i);
}
for (int k = 1; k < (1 << n); k++)
{
for (int i = 1; i <= n; i++)
{
if ((k & 1 << (i - 1)) == 0)
{
continue;
}
for (int j = 1; j <= n; j++)
{
if (i == j)
{
continue;
}
if ((k & 1 << (j - 1)) == 0)
{
continue;
}
f[i][k] = min(f[i][k], f[j][k - (1 << (i - 1))] + dis(i, j));
}
}
}
double ans = 99999999;
for (int i = 1; i <= n; i++)
{
ans = min(ans, f[i][(1 << n) - 1]);
}
printf("%.2lf", ans);
return 0;
}
例三:关灯问题
现有 n 盏灯,以及 m 个按钮。每个按钮可以同时控制这 n 盏灯——按下了第 i 个按钮,对于所有的灯都有一个效果。按下 i 按钮对于第 j 盏灯,是下面 3 中效果之一:如果 ai,j 为 1,那么当这盏灯开了的时候,把它关上,否则不管;如果为 −1 的话,如果这盏灯是关的,那么把它打开,否则也不管;如果是 0,无论这灯是否开,都不管。
现在这些灯都是开的,给出所有开关对所有灯的控制效果,求问最少要按几下按钮才能全部关掉。
输入格式
前两行两个数,n,m。
接下来 m 行,每行 n 个数 ,ai,j 表示第 i 个开关对第 j 个灯的效果。
输出格式
一个整数,表示最少按按钮次数。如果没有任何办法使其全部关闭,输出 −1。
思路:将灯的状态用二进制模拟,利用bfs遍历搜索。
代码:
#include<iostream>
#include<queue>
using namespace std;
int n, m;
int a[105][15];
bool used[1 << 15];
struct node
{
int ss;
int step;
};
int main()
{
cin >> n;
cin >> m;
queue<node>q;
for (int i = 1; i <= m; i++)
{
for (int j = 1; j <= n; j++)
{
cin >> a[i][j];
}
}
q.push({ (1 << n) - 1, 0 });
used[(1 << n) - 1] = true;
int s = -1, step;
while (!q.empty())
{
node ss = q.front();
s = ss.ss;
if (s == 0)
{
step = ss.step;
break;
}
q.pop();
for (int i = 1; i <= m; i++)
{
s = ss.ss;
step = ss.step;
for (int j = 1; j <= n; j++)
{
if (a[i][j] == 1 && (1 << (j - 1) & s))
{
s ^= 1 << (j - 1);
}
else if (a[i][j] == -1 && !(1 << (j - 1) & s))
{
s |= 1 << (j - 1);
}
}
if (!used[s])
{
used[s] = true;
step++;
q.push({ s,step });
}
}
}
if (s == 0)
{
cout << step;
}
else
{
cout << -1;
}
return 0;
}