信奥赛C++提高组csp-s之状压DP详解及编程实例

一、状态压缩DP的核心思想
状态压缩动态规划(简称"状压DP")是一种利用二进制位运算压缩状态空间的动态规划方法。适用于状态维度较高但每个维度状态数较少的场景(如每个位置只有选/不选两种状态)。
二、核心知识点
-
状态表示:
-
用整数的二进制位表示状态,例如:
cppmask = 0b101 表示第1、3个位置被选中
-
-
常用位运算:
cpp(s >> (k-1)) & 1 // 检查第k位是否为1 s |= (1 << (k-1)) // 设置第k位为1 s &= ~(1 << (k-1)) // 设置第k位为0 s1 & s2 // 检测状态冲突 __builtin_popcount(s) // 统计1的个数(GCC) -
状态设计:
dp[i][mask]通常表示前i行/位置,且当前状态为mask时的最优解
在算法竞赛中,位运算的巧妙使用可以极大提升代码效率和简洁性。以下是一些实用的小技巧和原理说明:
基础但关键的技巧
-
判断奇偶性:
cppif (n & 1) // 等价于 n % 2 == 1- 原理:二进制最低位为1时是奇数
-
交换两个数:
cppa ^= b; b ^= a; a ^= b; // 无需临时变量- 原理 :利用异或的自反性
a ^ a = 0
- 原理 :利用异或的自反性
-
取相反数:
cppint negative = ~n + 1; // 等价于 -n- 原理:二进制补码表示法
进阶状态处理技巧
-
快速获取最低位的1:
cppint lowbit = x & (-x); // 示例:0b10100 -> 0b100- 应用:树状数组核心操作
-
遍历所有子集:
cppfor(int subset = s; subset; subset = (subset-1)&s) { // 处理subset }- 示例:s=0b101时遍历顺序 0b101→0b100→0b001
-
统计二进制中1的个数:
cppint count = __builtin_popcount(n); // GCC内置函数 int count = bitset<32>(n).count(); // C++标准库
状压DP中的黑科技
-
快速判断相邻位冲突:
cppif (s & (s << 1)) // 检测是否有相邻的1- 应用:棋盘类问题(如互不侵犯)
-
枚举合法状态转移:
cpp// 筛选有效状态示例 vector<int> valid; for(int s=0; s<(1<<n); ++s){ if(s & (s<<1)) continue; // 排除相邻1 valid.push_back(s); } -
快速状态合法性验证:
cpp// 检查状态s是否与掩码mask匹配 bool valid = (s | mask) == mask;
性能对比表
| 操作 | 常规方法 | 位运算方法 | 加速比 |
|---|---|---|---|
| 判断奇偶 | n%2 == 1 |
n&1 |
3x |
| 统计1的个数 | 循环统计 | __builtin_popcount |
10x |
| 子集遍历 | 递归生成 | 位运算递减 | 5x |
| 相邻冲突检测 | 逐位比较 | 位移与位与 | 8x |
三、经典模型与例题分析1
洛谷P1879 [USACO06NOV]Corn Fields G(难度:普及+/提高)
代码实现:
cpp
#include <bits/stdc++.h>
using namespace std;
const int MOD = 1e8;
int m, n;
int field[15]; // 存储每行的土地状态(二进制压缩)
int dp[15][1<<12]; // dp[i][s] 表示第i行状态为s时的方案数
int main() {
cin >> m >> n;
for (int i = 1; i <= m; i++) {
int state = 0;
for (int j = 0; j < n; j++) {
int x; cin >> x;
state = (state << 1) | x; // 将土地状态压缩为二进制
}
field[i] = state;
}
dp[0][0] = 1; // 初始化
for (int i = 1; i <= m; i++) {
for (int s = 0; s < (1<<n); s++) {
if ((s & field[i]) != s) continue; // 状态s必须全部在肥沃格子上
if (s & (s << 1)) continue; // 状态s自身不能有相邻的1
for (int prev = 0; prev < (1<<n); prev++) {
if ((s & prev) == 0) { // 上下两行状态不冲突
dp[i][s] = (dp[i][s] + dp[i-1][prev]) % MOD;
}
}
}
}
int ans = 0;
for (int s = 0; s < (1<<n); s++)
ans = (ans + dp[m][s]) % MOD;
cout << ans;
return 0;
}
功能模块分解
1. 输入处理与状态压缩
-
功能:将每行的土地状态(0/1)转换为二进制整数。
-
代码片段:
cppfor (int i = 1; i <= m; i++) { int state = 0; for (int j = 0; j < n; j++) { int x; cin >> x; state = (state << 1) | x; } field[i] = state; } -
说明:
- 将每行的输入(例如
1 0 1)压缩为二进制整数(如0b101,即十进制5)。 field[i]表示第i行的合法土地掩码。
- 将每行的输入(例如
2. 动态规划初始化
-
功能:初始化虚拟的第0行状态。
-
代码片段:
cppdp[0][0] = 1; -
说明:
- 第0行是虚拟行,表示没有放置奶牛,此时方案数为1(空状态)。
3. 状态转移
-
功能:逐行枚举所有可能的状态,并检查合法性。
-
代码片段:
cppfor (int s = 0; s < (1<<n); s++) { if ((s & field[i]) != s) continue; // 必须全部在肥沃格子上 if (s & (s << 1)) continue; // 同一行不能有相邻的1 for (int prev = 0; prev < (1<<n); prev++) { if ((s & prev) == 0) { // 上下两行不能有相邻的1 dp[i][s] += dp[i-1][prev]; } } } -
关键检查条件:
- 土地合法性 :状态
s的二进制位必须全为肥沃土地(即s是field[i]的子集)。 - 行内无冲突 :状态
s的二进制位不能有相邻的1(通过s & (s << 1)检测)。 - 行间无冲突 :当前行状态
s与上一行状态prev不能有上下相邻的1(通过s & prev == 0检测)。
- 土地合法性 :状态
4. 结果汇总
-
功能:统计最后一行的所有合法状态的总方案数。
-
代码片段:
cppint ans = 0; for (int s = 0; s < (1<<n); s++) ans = (ans + dp[m][s]) % MOD; -
说明:将所有可能的最终状态累加,得到总方案数。
关键算法分析
-
状态压缩:
- 每行的状态用二进制数表示(例如
0b101表示第1、3列放置奶牛)。 - 状态总数最多为 ( 2 12 ^{12} 12 = 4096 ),适合动态规划。
- 每行的状态用二进制数表示(例如
-
合法性检查:
- 行内检查 :通过
s & (s << 1)快速判断是否有相邻的1。 - 土地适配检查 :通过
(s & field[i]) == s确保所有放置位置都是肥沃土地。
- 行内检查 :通过
示例验证
假设输入为:
2 3
1 1 1
1 1 1
- 可能的合法状态 :
- 第1行:
0b000(空)、0b001、0b010、0b100、0b101。 - 第2行需与第1行状态不冲突。最终总方案数为 17。
- 第1行:
四、经典模型与例题分析2
洛谷P1896 [SCOI2005]互不侵犯(难度:普及+/提高)
问题描述:在N×N的棋盘放置K个国王,要求任意两个国王不互相攻击。
代码实现:
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll dp[10][1024][90]; // dp[i][mask][cnt]
int n, k;
vector<int> valid_states;
// 检查单行合法性
bool check(int s) {
return !(s & (s << 1)) && !(s & (s >> 1));
}
// 生成所有合法行状态
void preprocess() {
for(int s=0; s<(1<<n); ++s) {
if(check(s)) valid_states.push_back(s);
}
}
int main() {
cin >> n >> k;
preprocess();
// 初始化第一行
for(auto s : valid_states) {
int cnt = __builtin_popcount(s);
if(cnt <= k) dp[0][s][cnt] = 1;
}
// 状态转移
for(int i=1; i<n; ++i) {
for(auto cur : valid_states) {
int cnt_cur = __builtin_popcount(cur);
for(int prev : valid_states) {
if((cur & prev) || (cur & (prev<<1)) || (cur & (prev>>1)))
continue;
for(int c=0; c <= k - cnt_cur; ++c) {
dp[i][cur][c + cnt_cur] += dp[i-1][prev][c];
}
}
}
}
// 统计答案
ll ans = 0;
for(auto s : valid_states)
ans += dp[n-1][s][k];
cout << ans;
return 0;
}
功能模块详解
1. 数据结构定义
-
dp[10][1024][90]三维动态规划数组,维度含义:
- 第一维 :处理到第
i行(棋盘共n行,i ∈ [0, n-1]) - 第二维 :当前行的状态压缩值
mask(最大状态数为2^9 = 512,但合法状态更少) - 第三维 :已放置的国王总数
cnt(最多k个)
值表示:当前状态下的合法方案数。
- 第一维 :处理到第
-
valid_states存储所有单行合法的状态(二进制无相邻1)。
2. 预处理合法状态
-
check(int s)检查单行状态
s是否合法:cpp!(s & (s << 1)) && !(s & (s >> 1)- 若
s的二进制位有相邻的1(如0b110),则返回false。
- 若
-
preprocess()遍历所有可能的单行状态
s ∈ [0, 1<<n),筛选出合法状态存入valid_states。
3. 初始化第一行
cpp
for(auto s : valid_states) {
int cnt = __builtin_popcount(s);
if(cnt <= k) dp[0][s][cnt] = 1;
}
- 对每个合法状态
s,计算其包含的国王数量cnt(即二进制中1的个数)。 - 若
cnt ≤ k,则在第一行放置该状态的方案数为1。
4. 状态转移
cpp
for(int i=1; i<n; ++i) {
for(auto cur : valid_states) { // 当前行状态
int cnt_cur = __builtin_popcount(cur);
for(int prev : valid_states) { // 前一行状态
// 检查纵向和斜向冲突
if((cur & prev) || (cur & (prev<<1)) || (cur & (prev>>1))) continue;
// 遍历可能的国王数量
for(int c=0; c <= k - cnt_cur; ++c) {
dp[i][cur][c + cnt_cur] += dp[i-1][prev][c];
}
}
}
}
- 冲突检测 :
cur & prev:检查上下相邻列是否有冲突。cur & (prev << 1):检查当前行与上一行左移一位后的斜向冲突。cur & (prev >> 1):检查当前行与上一行右移一位后的斜向冲突。
- 状态转移 :
若状态无冲突,则将前一行prev状态下已放置c个国王的方案数,累加到当前行cur的c + cnt_cur位置。
5. 结果统计
cpp
ll ans = 0;
for(auto s : valid_states)
ans += dp[n-1][s][k];
cout << ans;
- 遍历最后一行的所有合法状态
s,累加已放置k个国王的方案数。
关键设计思想
-
状态压缩
将每行的状态用二进制数表示(1表示放置国王),将指数级的状态空间压缩到多项式级别。
-
合法状态筛选
预处理所有单行合法状态,减少无效状态转移。
-
三维状态设计
dp[i][mask][cnt]精确记录行数、当前行状态、总国王数三个维度,确保转移无遗漏。
复杂度分析
-
时间复杂度 :
( O(n ⋅ S 2 ⋅ k \cdot S^2 \cdot k ⋅S2⋅k) ),其中 ( S ) 为合法状态数(约60)。
当 ( n=9, S≈60, k=81 ) 时,计算量约为 ( 9 ⋅ 60 2 ⋅ 81 ≈ 2.6 × 10 6 9 \cdot 60^2 \cdot 81 ≈ 2.6 \times 10^6 9⋅602⋅81≈2.6×106 ),完全可接受。
-
空间复杂度 :
( O(n ⋅ S ⋅ k \cdot S \cdot k ⋅S⋅k) ),当 ( n=9, S=60, k=81 ) 时,占用约 ( 9 ⋅ 60 ⋅ 81 ⋅ \cdot 60 \cdot 81 \cdot ⋅60⋅81⋅ 8B ≈ 349KB )。
优化思考
-
滚动数组优化
由于当前行只依赖前一行,可将
dp数组压缩为二维dp[2][S][k],将空间复杂度降至 ( O(S ⋅ k \cdot k ⋅k) )。 -
剪枝优化
在状态转移时,若
c + cnt_cur > k可直接跳过,减少无效计算。
五、洛谷OJ练习题单
- P1896 [SCOI2005]互不侵犯(基础状压DP)
- P1879 [USACO06NOV]Corn Fields G(状态合法性处理)
- P2704 [NOI2001]炮兵阵地(三维状态设计)
- P2622 关灯问题II(状态转移与位运算)
- P3092 [USACO13NOV]No Change G(状态压缩+前缀和)
- P2157 [SDOI2009]学校食堂(复杂状态设计)
- P1433 吃奶酪(经典TSP问题)
- P4011 孤岛营救问题(分层图状压)
更多系列知识,请查看专栏:《信奥赛C++提高组csp-s知识详解及案例实践》:
https://blog.csdn.net/weixin_66461496/category_13113932.html
各种学习资料,助力大家一站式学习和提升!!!
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
cout<<"########## 一站式掌握信奥赛知识! ##########";
cout<<"############# 冲刺信奥赛拿奖! #############";
cout<<"###### 课程购买后永久学习,不受限制! ######";
return 0;
}
1、csp信奥赛高频考点知识详解及案例实践:
CSP信奥赛C++动态规划:
https://blog.csdn.net/weixin_66461496/category_13096895.html点击跳转
CSP信奥赛C++标准模板库STL:
https://blog.csdn.net/weixin_66461496/category_13108077.html 点击跳转
信奥赛C++提高组csp-s知识详解及案例实践:
https://blog.csdn.net/weixin_66461496/category_13113932.html
2、csp信奥赛冲刺一等奖有效刷题题解:
CSP信奥赛C++初赛及复赛高频考点真题解析(持续更新):https://blog.csdn.net/weixin_66461496/category_12808781.html 点击跳转
CSP信奥赛C++一等奖通关刷题题单及题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12673810.html 点击跳转
3、GESP C++考级真题题解:

GESP(C++ 一级+二级+三级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12858102.html 点击跳转

GESP(C++ 四级+五级+六级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12869848.html 点击跳转

GESP(C++ 七级+八级)真题题解(持续更新):
https://blog.csdn.net/weixin_66461496/category_13117178.html
4、CSP信奥赛C++竞赛拿奖视频课:
https://edu.csdn.net/course/detail/40437 点击跳转

· 文末祝福 ·
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
cout<<"跟着王老师一起学习信奥赛C++";
cout<<" 成就更好的自己! ";
cout<<" csp信奥赛一等奖属于你! ";
return 0;
}