一、问题引入
动态规划的状态设计直接决定了算法的复杂度。在某些问题中,状态可以用一个二进制数紧凑地表示——这就是状态压缩动态规划(State Compression DP)。
状态压缩DP最常用于处理集合相关的决策问题,例如旅行商问题(TSP)、集合覆盖、分配问题等。本文将以经典的旅行商问题(TSP)为例,用Java实现状态压缩DP,并深入讲解位运算在状态转移中的优化技巧。
二、算法原理
2.1 什么是状态压缩?
假设有N个城市,我们需要记录哪些城市已经被访问过。如果用布尔数组visited[N]表示,DP状态就是dp[visited[]][current_city],这在实现上很不方便。
如果用二进制数state代替visited数组:
– state的第i位为1,表示第i个城市已访问
– state的第i位为0,表示第i个城市未访问
这样,N个城市的访问状态就可以用一个N位整数表示,DP状态简化为dp[state][current_city]。
2.2 TSP问题定义
给定N个城市和两两之间的距离,从城市0出发,访问所有城市恰好一次后回到起点,求最短路径长度。
状态定义:dp[state][i] 表示已经访问了state中标记的城市,且当前位于城市i的最短距离。
状态转移:
dp[state][i] = min(dp[state ^ (1<<i)][j] + dist[j][i])
其中 j 是state中除了i之外的所有已访问城市
三、Java代码实现
import java.util.*;
/**
* 状态压缩动态规划
* 以旅行商问题(TSP)为例
*/
public class StateCompressionDP {
private int n;
private int[][] dist;
private int INF = Integer.MAX_VALUE / 2;
public StateCompressionDP(int[][] dist) {
this.n = dist.length;
this.dist = dist;
}
/**
* TSP求解 - 状态压缩DP
* 时间复杂度:O(N^2 * 2^N)
* 空间复杂度:O(N * 2^N)
*
* @return 最短回路长度
*/
public int solveTSP() {
int fullState = (1 << n) - 1; // 所有位都为1,表示所有城市都已访问
// dp[state][i]: 已访问state中的城市,当前在i的最短距离
int[][] dp = new int[1 << n][n];
// 初始化
for (int[] row : dp) {
Arrays.fill(row, INF);
}
// 从城市0出发
dp[1][0] = 0; // state=1(只有第0位为1),当前在城市0
// 枚举所有状态
for (int state = 1; state <= fullState; state++) {
// 枚举当前所在城市
for (int i = 0; i < n; i++) {
if (dp[state][i] == INF) continue;
if ((state & (1 << i)) == 0) continue; // i不在state中,跳过
// 尝试前往未访问的城市j
for (int j = 0; j < n; j++) {
if ((state & (1 << j)) != 0) continue; // j已访问,跳过
int nextState = state | (1 << j);
int newDist = dp[state][i] + dist[i][j];
if (newDist < dp[nextState][j]) {
dp[nextState][j] = newDist;
}
}
}
}
// 找最短的回到起点的路径
int minTour = INF;
for (int i = 1; i < n; i++) {
if (dp[fullState][i] != INF) {
minTour = Math.min(minTour, dp[fullState][i] + dist[i][0]);
}
}
return minTour;
}
/**
* TSP求解 - 带路径记录
* @return 包含最短距离和路径的结果
*/
public TSPResult solveTSPWithPath() {
int fullState = (1 << n) - 1;
int[][] dp = new int[1 << n][n];
int[][] parent = new int[1 << n][n]; // 记录前驱城市
for (int[] row : dp) Arrays.fill(row, INF);
for (int[] row : parent) Arrays.fill(row, -1);
dp[1][0] = 0;
for (int state = 1; state <= fullState; state++) {
for (int i = 0; i < n; i++) {
if (dp[state][i] == INF) continue;
if ((state & (1 << i)) == 0) continue;
for (int j = 0; j < n; j++) {
if ((state & (1 << j)) != 0) continue;
int nextState = state | (1 << j);
int newDist = dp[state][i] + dist[i][j];
if (newDist < dp[nextState][j]) {
dp[nextState][j] = newDist;
parent[nextState][j] = i;
}
}
}
}
// 找最优终点
int minTour = INF;
int lastCity = -1;
for (int i = 1; i < n; i++) {
if (dp[fullState][i] != INF) {
int tourLen = dp[fullState][i] + dist[i][0];
if (tourLen < minTour) {
minTour = tourLen;
lastCity = i;
}
}
}
// 回溯路径
List<Integer> path = new ArrayList<>();
int state = fullState;
int curr = lastCity;
while (curr != -1) {
path.add(curr);
int prev = parent[state][curr];
state = state ^ (1 << curr);
curr = prev;
}
Collections.reverse(path);
return new TSPResult(minTour, path);
}
static class TSPResult {
int distance;
List<Integer> path;
TSPResult(int distance, List<Integer> path) {
this.distance = distance;
this.path = path;
}
}
/**
* 子集枚举工具方法
* 枚举state的所有非空子集
*/
public static void enumerateSubsets(int state) {
System.out.println("State " + state + " (binary: " +
String.format("%" + Integer.SIZE + "s",
Integer.toBinaryString(state)).replace(' ', '0') + ") 的子集:");
// 枚举子集:for (int sub = state; sub > 0; sub = (sub - 1) & state)
for (int sub = state; sub > 0; sub = (sub - 1) & state) {
System.out.println(" 子集: " + sub + " (" +
Integer.toBinaryString(sub) + ")");
}
}
/**
* 位运算技巧演示
*/
public static void bitOperationsDemo() {
System.out.println("=== 位运算技巧 ===");
int state = 0b10110; // 二进制:10110
System.out.println("原始状态: " + Integer.toBinaryString(state));
System.out.println("第1位是否为1: " + ((state & (1 << 1)) != 0));
System.out.println("第2位是否为1: " + ((state & (1 << 2)) != 0));
System.out.println("设置第0位为1: " + Integer.toBinaryString(state | (1 << 0)));
System.out.println("清除第1位: " + Integer.toBinaryString(state & ~(1 << 1)));
System.out.println("翻转第2位: " + Integer.toBinaryString(state ^ (1 << 2)));
System.out.println("最低位的1: " + Integer.toBinaryString(state & (-state)));
System.out.println("清除最低位的1: " + Integer.toBinaryString(state & (state - 1)));
System.out.println("1的个数: " + Integer.bitCount(state));
}
/**
* 集合覆盖问题的状态压缩DP
* 给定若干集合,选择最少数量的集合使其并集为全集
*/
public int solveSetCover(int[] sets, int numElements) {
int fullMask = (1 << numElements) - 1;
int[] dp = new int[1 << numElements];
Arrays.fill(dp, INF);
dp[0] = 0; // 覆盖空集需要0个集合
for (int mask = 0; mask <= fullMask; mask++) {
if (dp[mask] == INF) continue;
for (int set : sets) {
int newMask = mask | set;
if (dp[newMask] > dp[mask] + 1) {
dp[newMask] = dp[mask] + 1;
}
}
}
return dp[fullMask];
}
public static void main(String[] args) {
// TSP测试
int[][] dist = {
{0, 10, 15, 20},
{10, 0, 35, 25},
{15, 35, 0, 30},
{20, 25, 30, 0}
};
System.out.println("=== TSP问题 ===");
StateCompressionDP solver = new StateCompressionDP(dist);
int minTour = solver.solveTSP();
System.out.println("最短回路长度: " + minTour);
TSPResult result = solver.solveTSPWithPath();
System.out.println("最短路径: " + result.path + " -> 0");
System.out.println("路径长度: " + result.distance);
// 位运算演示
System.out.println("\n=== 位运算技巧 ===");
bitOperationsDemo();
// 子集枚举演示
System.out.println("\n=== 子集枚举 ===");
enumerateSubsets(0b1011);
// 集合覆盖测试
System.out.println("\n=== 集合覆盖问题 ===");
int[] sets = {
0b0011, // 集合1覆盖元素0,1
0b0110, // 集合2覆盖元素1,2
0b1100, // 集合3覆盖元素2,3
0b1001 // 集合4覆盖元素0,3
};
int minSets = solver.solveSetCover(sets, 4);
System.out.println("覆盖所有元素最少需要 " + minSets + " 个集合");
}
}
四、复杂度分析
| 问题 | 状态数 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|
| TSP | 2^N × N | O(N² × 2^N) | O(N × 2^N) |
| 集合覆盖 | 2^N | O(M × 2^N) | O(2^N) |
状态压缩DP的状态数呈指数级增长,因此通常只适用于N ≤ 20的问题。对于更大的规模,需要结合其他优化策略(如分支限界、启发式搜索等)。
五、位运算核心技巧
| 操作 | 代码 | 含义 |
|---|---|---|
| 判断第i位 | (state >> i) & 1 |
检查第i位是否为1 |
| 设置第i位 | state \| (1 << i) |
将第i位置为1 |
| 清除第i位 | state & ~(1 << i) |
将第i位置为0 |
| 翻转第i位 | state ^ (1 << i) |
第i位取反 |
| 枚举子集 | for (sub = state; sub; sub = (sub-1) & state) |
遍历所有非空子集 |
| 最低位1 | state & (-state) |
提取最低位的1 |
| 1的个数 | Integer.bitCount(state) |
统计二进制中1的个数 |
六、总结
状态压缩DP将集合状态巧妙地编码为整数,使得原本需要高维数组的DP问题可以紧凑地表示。它是理解位运算与动态规划结合的绝佳切入点,也是解决中小规模集合决策问题的利器。
掌握状态压缩DP后,读者可以将其应用于更多场景:棋盘覆盖、任务分配、子集选择等问题都可以用这个框架求解。关键在于识别出问题中的”集合选择”特征,并将选择状态正确地映射为二进制编码。