概念
简而言之:是处理复杂集合问题的 DP。每个状态 dp[i][j] 表示的不是一个有意义的数值,例如花费、价值、长度等,而是代表了集合的数量
旅行商问题
Traveling Saleman Problem, TSP:有 n 个城市,已知任何两个城市之间的距离(或者费用),一个旅行商从某城市出发,经过每一个城市并且只经过一次,最后回到出发城市,输出最短(或者路费最少)的线路
小规模的 TSP 问题可以用状态压缩 DP 求解,复杂度是:,能解决规模 n<=15 的问题
思路:假设最短的 TSP 路径是:
那么
所以,问题转变为:求经过所有城市的最短回路 从某个城市到起点的最短路径
DP 状态设计如下:假设已经访问过的城市集合是 S,当前所在城市是 v,用 dp[S][v] 表示从 v 出发访问剩余的所有城市最后回到起点的路径费用总和的最小值。
状态转移方程: //V 是最后一个城市
城市集合 S 如何表示?这就用到压缩状态 DP 的技巧;把路径“压缩”到二进制数中。定义: int dp[1<<MAXN][MAXN];
MAXN 是城市数量,当 MAXN=15 时,1<<MAXN=2^15=32768, 0~32768 内的每个数的二进制表示就是一个可能的路径,二进制数中的 1 表示选中一个城市,0 表示不选中。例如:S=000 0000 0000 0101(2),末尾的 101 表示已访问过的城市2、0。在下面代码中,“dp[s | 1<<u][u]”,其中的 s | 1<<u,表示在已访问过的城市集合 S 中加入一个新访问的城市 u
下面是部分示意代码(c++):
例2
TSP 的变形
本题用状态压缩 DP 求解,算法复杂度是 ,当 n=10 时,正好通过 OJ 测试
- 路径的表示
在普通 TSP 中,一个城市只有两种情况,即访问和不访问,用 1 和 0 表示。这个题有 3 种情况,也就是 不访问、访问 1 次、访问 2 次,所以需要用到三进制
当 n=10 时有 种组合(路径数量),对每个路径用三进制表示。例如:第 14 中路径,它的三进制是 ,表示的是第 3 个城市走 1 次,第 2 个城市走 1 次,第 1 个城市走 2 次
在程序中用 tri[i][j],表示第 i 个路径,其第 j 位的值是城市的状态,例如:tri[14][3]=1, tri[14][2]=1, tri[14][1]=2
- 状态和状态转移
定义状态 dp[i][j],当前所在城市是 i,dp[i][j] 表示从 i 出发访问剩余的所有城市最后回到起点的路径 j 的费用总和的最小值
状态转移:
代码(Java)
public class Main
{
final int INF = 0x3f3f3f3f;
int n,m;
int[] bit = {0,1,3,9,27,81,243,729,2187,6561,19683,59049}; //三进制每一位的权值,与二进制的0、1、2、4、8等对照
int[][] tri = new int[60000][11];
int[][] dp = new int[11][60000];
int[][] graph = new int[11][11]; //存图
private void make_trb(){ //初始化
for(int i=0; i<59050; i++){ //共 3^10=59050 种状态
int t = i;
for(int j=1; j<=10; j++){
tri[i][j] = t%3;
t/=3;
}
}
}
private int comp_dp(){
int ans = INF;
for(int i=0; i<11; i++)
Arrays.fill(dp[i], INF);
for(int i=0; i<=n; i++)
dp[i][bit[i]] = 0; //bit[i] 是第 i 个城市,起点是任意的
for(int i=0; i<bit[n+1]; i++){ //此循环中,k、j都表示城市点,i、l是路径状态
boolean flag = true; //所有的城市都遍历1次以上
for(int j=1; j<=n; j++){ //选一个终点,即城市 j
if(tri[i][j]==0){ //判断终点位是否为0,详见注解第4点
flag = false; //还没有经过所有点
continue;
}
if(i==j)
continue;
for(int k=1; k<=n; k++){
int l = i - bit[j]; //i 状态的第 j 位置 0,l 为没有走过j城市前的i状态(用于递归),即去掉城市j,因要将j作为此路径终点
if(tri[i][k]==0) //判断k是否在i中
continue;
dp[j][i] = Math.min(dp[j][i], dp[k][l]+graph[k][j]);
}
}
if(flag) //在所有可行路径中找最小费用
for(int j=1; j<=n; j++)
ans = Math.min(ans,dp[j][i]);
}
return ans;
}
public static void main(String args[]){
Main m = new Main();
Scanner sc = new Scanner(System.in);
m.make_trb();
while (!sc.hasNext("#")){ //输入以"#"字符串结束
m.n = sc.nextInt();
m.m = sc.nextInt();
for(int i=0; i<11; i++)
Arrays.fill(m.graph[i],m.INF);
while(m.m--!=0){
int a = sc.nextInt(), b = sc.nextInt(), c = sc.nextInt();
if(c<m.graph[a][b])
m.graph[a][b] = m.graph[b][a] = c;
}
int ans = m.comp_dp();
if(ans==m.INF)
System.out.println("-1");
else System.out.println(ans);
}
sc.close();
}
}
对以上代码的注解:
- tri[i][j]:路径 i 中表示一个城市走过的次数
- dp[j][i]: 表示在路径状态 i 下(即走了多少点,以及每个点走了多少次),最后走的是 j 点的最小花费
- 对于一个已有状态 dp[j][s], 枚举每一个点 k,如果这个点是 k 不是 j ,而k,j 有边,且 k 被访问次数不超过 2 次,那么下一步就可访问 k 点
- 如何保证走过了所有城市?就是所选路径 i 中的每个数位均大于0(即对应的数组 tri[i] 中每一元素均大于0,则 i 是一个可行路径状态)
- 输入时数组 graph 中的 a、b 是从1 开始的