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

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

一、状态压缩DP的核心思想

状态压缩动态规划(简称"状压DP")是一种利用二进制位运算压缩状态空间的动态规划方法。适用于状态维度较高但每个维度状态数较少的场景(如每个位置只有选/不选两种状态)。

二、核心知识点
  1. 状态表示

    • 用整数的二进制位表示状态,例如:

      cpp 复制代码
      mask = 0b101 表示第1、3个位置被选中
  2. 常用位运算

    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)
  3. 状态设计

    • dp[i][mask] 通常表示前i行/位置,且当前状态为mask时的最优解

在算法竞赛中,位运算的巧妙使用可以极大提升代码效率和简洁性。以下是一些实用的小技巧和原理说明:


基础但关键的技巧
  1. 判断奇偶性

    cpp 复制代码
    if (n & 1)  // 等价于 n % 2 == 1
    • 原理:二进制最低位为1时是奇数
  2. 交换两个数

    cpp 复制代码
    a ^= b; b ^= a; a ^= b;  // 无需临时变量
    • 原理 :利用异或的自反性 a ^ a = 0
  3. 取相反数

    cpp 复制代码
    int negative = ~n + 1;  // 等价于 -n
    • 原理:二进制补码表示法

进阶状态处理技巧
  1. 快速获取最低位的1

    cpp 复制代码
    int lowbit = x & (-x);  // 示例:0b10100 -> 0b100
    • 应用:树状数组核心操作
  2. 遍历所有子集

    cpp 复制代码
    for(int subset = s; subset; subset = (subset-1)&s) {
        // 处理subset
    }
    • 示例:s=0b101时遍历顺序 0b101→0b100→0b001
  3. 统计二进制中1的个数

    cpp 复制代码
    int count = __builtin_popcount(n);      // GCC内置函数
    int count = bitset<32>(n).count();      // C++标准库

状压DP中的黑科技
  1. 快速判断相邻位冲突

    cpp 复制代码
    if (s & (s << 1))  // 检测是否有相邻的1
    • 应用:棋盘类问题(如互不侵犯)
  2. 枚举合法状态转移

    cpp 复制代码
    // 筛选有效状态示例
    vector<int> valid;
    for(int s=0; s<(1<<n); ++s){
        if(s & (s<<1)) continue; // 排除相邻1
        valid.push_back(s);
    }
  3. 快速状态合法性验证

    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)转换为二进制整数。

  • 代码片段

    cpp 复制代码
    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;
    }
  • 说明

    • 将每行的输入(例如 1 0 1)压缩为二进制整数(如 0b101,即十进制5)。
    • field[i] 表示第 i 行的合法土地掩码。

2. 动态规划初始化
  • 功能:初始化虚拟的第0行状态。

  • 代码片段

    cpp 复制代码
    dp[0][0] = 1;
  • 说明

    • 第0行是虚拟行,表示没有放置奶牛,此时方案数为1(空状态)。

3. 状态转移
  • 功能:逐行枚举所有可能的状态,并检查合法性。

  • 代码片段

    cpp 复制代码
    for (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];
            }
        }
    }
  • 关键检查条件

    1. 土地合法性 :状态 s 的二进制位必须全为肥沃土地(即 sfield[i] 的子集)。
    2. 行内无冲突 :状态 s 的二进制位不能有相邻的1(通过 s & (s << 1) 检测)。
    3. 行间无冲突 :当前行状态 s 与上一行状态 prev 不能有上下相邻的1(通过 s & prev == 0 检测)。

4. 结果汇总
  • 功能:统计最后一行的所有合法状态的总方案数。

  • 代码片段

    cpp 复制代码
    int ans = 0;
    for (int s = 0; s < (1<<n); s++) 
        ans = (ans + dp[m][s]) % MOD;
  • 说明:将所有可能的最终状态累加,得到总方案数。


关键算法分析
  1. 状态压缩

    • 每行的状态用二进制数表示(例如 0b101 表示第1、3列放置奶牛)。
    • 状态总数最多为 ( 2 12 ^{12} 12 = 4096 ),适合动态规划。
  2. 合法性检查

    • 行内检查 :通过 s & (s << 1) 快速判断是否有相邻的1。
    • 土地适配检查 :通过 (s & field[i]) == s 确保所有放置位置都是肥沃土地。

示例验证

假设输入为:

复制代码
2 3
1 1 1
1 1 1
  • 可能的合法状态
    • 第1行:0b000(空)、0b0010b0100b1000b101
    • 第2行需与第1行状态不冲突。最终总方案数为 17。
四、经典模型与例题分析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 个国王的方案数,累加到当前行 curc + cnt_cur 位置。

5. 结果统计
cpp 复制代码
ll ans = 0;
for(auto s : valid_states) 
    ans += dp[n-1][s][k];
cout << ans;
  • 遍历最后一行的所有合法状态 s,累加已放置 k 个国王的方案数。

关键设计思想
  1. 状态压缩

    将每行的状态用二进制数表示(1表示放置国王),将指数级的状态空间压缩到多项式级别。

  2. 合法状态筛选

    预处理所有单行合法状态,减少无效状态转移。

  3. 三维状态设计
    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 )。


优化思考
  1. 滚动数组优化

    由于当前行只依赖前一行,可将 dp 数组压缩为二维 dp[2][S][k],将空间复杂度降至 ( O(S ⋅ k \cdot k ⋅k) )。

  2. 剪枝优化

    在状态转移时,若 c + cnt_cur > k 可直接跳过,减少无效计算。


五、洛谷OJ练习题单
  1. P1896 [SCOI2005]互不侵犯(基础状压DP)
  2. P1879 [USACO06NOV]Corn Fields G(状态合法性处理)
  3. P2704 [NOI2001]炮兵阵地(三维状态设计)
  4. P2622 关灯问题II(状态转移与位运算)
  5. P3092 [USACO13NOV]No Change G(状态压缩+前缀和)
  6. P2157 [SDOI2009]学校食堂(复杂状态设计)
  7. P1433 吃奶酪(经典TSP问题)
  8. 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;
}
相关推荐
轩情吖2 小时前
Qt多元素控件之QTableWidget
开发语言·c++·qt·表格·控件·qtablewidget
张张努力变强2 小时前
C++ 类和对象(五):初始化列表、static、友元、内部类等7大知识点全攻略
开发语言·数据结构·c++·算法
草莓熊Lotso2 小时前
Qt 显示与输入类控件进阶:数字、进度、输入框实战攻略
java·大数据·开发语言·c++·人工智能·qt
HellowAmy2 小时前
我的C++规范 - 指针指向
开发语言·c++·代码规范
小屁猪qAq2 小时前
ROS2 节点中使用参数
开发语言·c++·参数·ros2
CSDN_RTKLIB2 小时前
多线程锁基础
c++
坐怀不乱杯魂2 小时前
Linux网络 - Socket编程(IPv4&IPv6)
linux·服务器·网络·c++·udp·tcp
Yupureki2 小时前
《算法竞赛从入门到国奖》算法基础:搜索-BFS初识
c语言·数据结构·c++·算法·visual studio·宽度优先
CSDN_RTKLIB4 小时前
两版本锁抛出异常测试
c++