状压DP(又称“状态压缩动态规划”)是一种动态规划的变体,广泛应用于涉及状态压缩问题的场景,比如图的状态、子集问题等。它通过压缩状态表示来降低复杂度,从而使得解决问题的方式更加高效。
算法思想
状压DP的核心思想是使用一个位掩码(bitmask)来表示某些特定的状态。位掩码可以有效地表示集合的子集,尤其适合处理相对较小数量(通常不超过20个)的元素。
算法流程
-
状态表示:使用一个整数的二进制表示来表示状态。例如,若有N个元素,考虑一个不超过N的子集,则可以用一个N位的二进制数表示,位为1表示在集合中,位为0表示不在集合中。
-
状态转移:定义状态转移方程,通过对当前状态进行某种操作(比如加入新的元素或者移除元素)来得到新的状态。
-
初始化:在算法的开始阶段初始化DP数组,通常第一个状态是空集。
-
遍历所有状态:使用循环遍历所有可能的状态,计算每个状态的最佳解。
-
输出结果:最后可以从DP数组中获取最终解。
例题
在 N*N 的棋盘里面放 K 个国王,使他们互不攻击,共有多少种摆放方案。
国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共 8个格子。
#include<bits/stdc++.h>
using namespace std;
// 检查当前行的状态与前一行的状态是否兼容
bool isValid(int prevMask, int currMask, int N){
for (int i = 0; i < N; ++i){
if ((currMask & (1 << i)) != 0){ // 当前行的第 i 列有国王
// 检查前一行的第 i 列、第 i-1 列和第 i+1 列是否有国王
if ((prevMask & (1 << i)) != 0 ||
(i > 0 && (prevMask & (1 << (i - 1))) != 0) ||
(i < N - 1 && (prevMask & (1 << (i + 1))) != 0)){
return false;
}
// 检查当前行的第 i-1 列和第 i+1 列是否有国王
if ((i > 0 && (currMask & (1 << (i - 1))) != 0) ||
(i < N - 1 && (currMask & (1 << (i + 1))) != 0)){
return false;
}
}
}
return true;
}
long long countKingPlacements(int N, int K){
vector<vector<vector<long long>>> dp(N + 1, vector<vector<long long>>(1 << N, vector<long long>(K + 1, 0)));
dp[0][0][0] = 1; // 初始状态:第 0 行,无国王,已放置 0 个国王
// 逐行计算
for (int row = 1; row <= N; ++row){
for (int prevMask = 0; prevMask < (1 << N); ++prevMask){
for (int k = 0; k <= K; ++k){
if (dp[row - 1][prevMask][k] <= 0) continue;
for (int currMask = 0; currMask < (1 << N); ++currMask){
if (!isValid(prevMask, currMask, N)) continue;
int newKings = __builtin_popcount(currMask); // 计算当前行的国王数量
if (k + newKings <= K){
dp[row][currMask][k + newKings] += dp[row - 1][prevMask][k];
}
}
}
}
}
long long result = 0;
for (int mask = 0; mask < (1 << N); ++mask){
result += dp[N][mask][K];
}
return result;
}
int main(){
int N, K;
cin >> N >> K;
cout << countKingPlacements(N, K) << endl;
return 0;
}
在H市中心,由于交通拥堵,手动调整信号灯。每个操作杆影响k个信号灯,功能用长度为k的字符串表示,包括“+”表示顺时针变化、“-”表示逆时针变化以及“0”表示无影响。任务是从n个操作杆中选择任意组合,统计所有可能的信号灯组合,并计算每种组合的实现方式的数量,结果需对模数M取模。输入包括测试组数T,操作杆数n,信号灯数k及模数M,以及每个操作杆的功能字符串。输出要求按字典序列出所有信号灯组合及其对应的操作杆使用方法数。
#include <bits/stdc++.h>
using namespace std;
#define int long long
int Hash(vector<int> &v) {
int val = 0;
for (int i : v) val = val * 3 + i;
return val;
}
vector<int> inHash(int val, int k) {
vector<int> v(k);
for (int i = 0; i < k; i++) v[k - i - 1] = val % 3, val /= 3;
return v;
}
void add(vector<int> &res, string &s, bool flag = true) {
const int n = s.size();
for (int i = 0; i < n; i++) {
if (s[i] == '0') continue;
if (flag)
res[i] += s[i] == '+' ? 1 : 2;
else
res[i] += s[i] == '+' ? 2 : 1;
}
for (int &i : res) i %= 3;
}
void print(vector<int> res) {
const int n = res.size();
for (int i : res) cout << (char)('A' + i);
cout << ' ';
}
signed main() {
int task;
cin >> task;
while (task--) {
int n, k, mod;
cin >> n >> k >> mod;
vector<string> vs(n);
for (string &s : vs) cin >> s;
int len = pow(3, k);
vector<array<int, 2>> dp(len);
vector<array<int, 2>> vis(len);
bool f = true;
vis[0][f] = true;
dp[0][f] = 1;
for (string s : vs) {
for (int i = 0; i < len; i++)
dp[i][f ^ 1] = vis[i][f ^ 1] = 0;
for (int i = 0; i < len; i++) {
if (vis[i][f] == false) continue;
dp[i][f ^ 1] = (dp[i][f ^ 1] + dp[i][f]) % mod;
vector<int> v = inHash(i, k);
add(v, s);
int x = Hash(v);
dp[x][f ^ 1] = (dp[x][f ^ 1] + dp[i][f]) % mod;
vis[i][f^1]=vis[x][f^1] = true;
}
f ^= 1;
}
for (int i = 0; i < len; i++) {
if (!vis[i][f]) continue;
vector<int> v = inHash(i, k);
print(v);
cout << dp[i][f] << endl;
}
}
return 0;
}