状压DP 详解教程 简单易学(bushi

状压DP补档

一、基本概念

  1. 什么是状压DP

状态压缩动态规划(State Compression Dynamic Programming)是一种通过二进制或其他紧凑表示方式来优化状态空间的动态规划方法。它通常用于解决状态可以表示为集合或排列的问题。

  1. 适用场景
    状态可以表示为集合(如选/不选某些元素)

状态维度较高但每个维度状态较少(如棋盘覆盖问题)

需要记录访问历史或选择历史的问题

  1. 核心思想
    用二进制数表示状态(0/1表示存在/不存在)

通过位运算高效地进行状态转移

将指数级的状态空间压缩为多项式级

二、常用位运算技巧

  1. 基本操作
cpp 复制代码
// 设置第i位为1
mask |= (1 << i);

// 设置第i位为0
mask &= ~(1 << i);

// 切换第i位
mask ^= (1 << i);

// 检查第i位是否为1
if (mask & (1 << i)) {...}

// 获取最低位的1
lowbit = mask & -mask;

// 清除最低位的1
mask &= (mask - 1);
2. 高级操作

// 遍历所有子集
for (int subset = mask; subset; subset = (subset - 1) & mask) {
    // 处理subset
}

// 检查mask是否是全1
if (mask == (1 << n) - 1) {...}

// 计算二进制中1的个数
int count = __builtin_popcount(mask); // GCC内置函数

三、经典问题模型

1. 旅行商问题(TSP)

状态表示

  • DP[M][U]:已经访问过M表示的城市集合,当前位于城市U的最短路径

状态转移

cpp 复制代码
DP[M][U] = min(DP[M][U], DP[M^(1<<U)][V] + D[V][U])

2. 棋盘覆盖问题

状态表示

  • 用二进制表示一行的覆盖状态
  • 可能需要多行状态共同表示

四、解题模板

1. 基本框架(ALLman风格)

cpp 复制代码
int DP[1 << N][...]; // 状态数组
memset(DP, INF, sizeof(DP)); // 初始化

// 初始状态
DP[INIT_M][...] = INIT_VAL;

// 状态转移
for (int M = 0; M < (1 << N); ++M)
{
    for (int I = 0; I < N; ++I)
    {
        if (!(M & (1 << I))) // 如果第I位未被选中
        {
            int NEW_M = M | (1 << I);
            DP[NEW_M][...] = UPDATE(DP[M][...], ...);
        }
    }
}

// 结果通常是DP[FULL_M][...]

2. 位运算操作(ALLman风格)

cpp 复制代码
// 设置第I位为1
M |= (1 << I);

// 设置第I位为0
M &= ~(1 << I);

// 切换第I位
M ^= (1 << I);

// 检查第I位是否为1
if (M & (1 << I)))
{
    ...
}

// 获取最低位的1
LB = M & -M;

// 清除最低位的1
M &= (M - 1);

五、例题精选

1. Leetcode 464. 我能赢吗

cpp 复制代码
bool CAN_I_WIN(int MAX_CHOOSE, int TARGET)
{
    if (MAX_CHOOSE >= TARGET)
    {
        return true;
    }
    if (MAX_CHOOSE * (MAX_CHOOSE + 1) / 2 < TARGET)
    {
        return false;
    }
    
    unordered_map<int, bool> MEMO;
    return DFS(0, 0, MAX_CHOOSE, TARGET, MEMO);
}

bool DFS(int USED, int SUM, int MAX_CHOOSE, int TARGET, unordered_map<int, bool>& MEMO)
{
    if (MEMO.count(USED))
    {
        return MEMO[USED];
    }
    
    for (int I = 1; I <= MAX_CHOOSE; ++I)
    {
        if (!(USED & (1 << I)))
        {
            if (SUM + I >= TARGET || !DFS(USED | (1 << I), SUM + I, MAX_CHOOSE, TARGET, MEMO))
            {
                return MEMO[USED] = true;
            }
        }
    }
    
    return MEMO[USED] = false;
}

2. Leetcode 691. 贴纸拼词

cpp 复制代码
int MIN_STICKERS(vector<string>& STICKERS, string TARGET)
{
    int N = TARGET.size();
    vector<int> DP(1 << N, -1);
    DP[0] = 0;
    
    for (int M = 0; M < (1 << N); ++M)
    {
        if (DP[M] == -1)
        {
            continue;
        }
        
        for (string& S : STICKERS)
        {
            int CUR = M;
            for (char C : S)
            {
                for (int I = 0; I < N; ++I)
                {
                    if (!(CUR & (1 << I)) && C == TARGET[I])
                    {
                        CUR |= (1 << I);
                        break;
                    }
                }
            }
            
            if (DP[CUR] == -1 || DP[CUR] > DP[M] + 1)
            {
                DP[CUR] = DP[M] + 1;
            }
        }
    }
    
    return DP[(1 << N) - 1];
}

3. Leetcode 1434. 戴帽子

cpp 复制代码
int NUMBER_WAYS(vector<vector<int>>& HATS)
{
    const int MOD = 1e9 + 7;
    int N = HATS.size();
    vector<vector<int>> DP(41, vector<int>(1 << N, 0));
    DP[0][0] = 1;
    
    for (int I = 1; I <= 40; ++I)
    {
        for (int M = 0; M < (1 << N); ++M)
        {
            DP[I][M] = DP[I - 1][M];
        }
        
        for (int P = 0; P < N; ++P)
        {
            if (find(HATS[P].begin(), HATS[P].end(), I) != HATS[P].end())
            {
                for (int M = 0; M < (1 << N); ++M)
                {
                    if (!(M & (1 << P)))
                    {
                        int NEW_M = M | (1 << P);
                        DP[I][NEW_M] = (DP[I][NEW_M] + DP[I - 1][M]) % MOD;
                    }
                }
            }
        }
    }
    
    return DP[40][(1 << N) - 1];
}

886