旅行商问题:
给定一个n个顶点组成的带权有向图的距离矩阵d(i,j),(INF表示没有边). 要求从顶点0出发,经过每个顶点恰好一次后再返回顶点0. 问所经过的边的总权重的最小值是多少?
n=5,权重如图所示。
这个就是著名的旅行商问题(TSP)。
所有可能的路线总共有 种,这是一个非常大的值,即使本题的n已经很小了。
对于这个问题,我们可以用dp来解决。
假设现在已经访问过的定点的集合(起点0当做还未访问过的顶点)为S,当前所在的顶点为v,用dp[S][v]表示从v出发访问剩余的所有节点,最终回到顶点0的路径的权重的总和的最小值。由于从v出发可以移动到任意一个节点 ,因此有如下递推式:
dp[v][0]=0
dp[S][v]=min{ dp[S∪{u}][u]+d(v,u)}, u∉ S
我们只要按照这个递推式进行计算就可以了,由于在递推式中有一个下标是集合,因此需要稍加处理。首先我们试着使用记忆化搜索求解。虽然有一个下标不是整数,但是我们可以把它编码成一个整数,从而使用记忆化搜索来求解。特别地,对于集合我们可以把每一个元素的选取与否对应到一个二进制位里,从而把状态压缩成一个整数,大大方便了计算和维护。这也就是状态压缩。
对于把每一个元素选取与否对应到二进制位的解释:
假如集合为 { 0 ,1 ,2, 3, 4},那么只选取0对应的二进制位就是00001,选取0,1,2就是00111.
n个顶点的状态总共有 (1<<n)-1 个 即n个1.
S>>u,即 S的状态里第u位之前是多少,S>>u&1 即判断第u位是不是1.
可以自己试一试
Java的21是10101,Integer.toBinaryString()方法可以将一个整数转成二进制。
代码如下:
package _状态压缩DP;
import java.util.Arrays;
import java.util.Scanner;
//状态压缩DP
/*
给定一个n个顶点组成的带权有向图的距离矩阵d(i,j),(INF表示没有边).
要求从顶点0出发,经过每个顶点恰好一次后再返回顶点0.
问所经过的边的总权重的最小值是多少?
* */
/*
由于在这个递推式中,有一个下标是集合而不是普通的整数,因此需要稍加处理。
首先我们试着使用记忆化搜索求解。虽然有一个下标不是整数,但是我们可以把它编码成整数。
或者给他们定义一个全序关系并用二叉搜索树存储,从而可以使用记忆化搜索。
特别地,对于集合我们可以把每一个元素的选取与否对应到一个个二进制位上,从而把状态压缩成一个整数,大大方便了计算和维护。
* */
//递推式:dp[v][0]=0 dp[s][v]表示从v出发访问剩余的顶点,最终回到0的总权重的最小值
// dp[S][v]=min{dp[S U {u}][u]+d(u,v)};
public class _旅行商问题 {
static int INF=100000;
static int n;//n个顶点
static int[][] d=new int[100][100];//图的权值矩阵
static int[][] dp=new int[100][100];//dp数组,记忆化
//已经访问过的集合S,当前顶点V
static int rec(int S,int v){
if (dp[S][v]>=0){
return dp[S][v];
}
if(S==(1<<n)-1&&v==0){
//已经访问过所有顶点并回到0顶点
return dp[S][v]=0;
}
int res=INF;
for (int u = 0; u <n ; u++) {
if((S>>u&1)==0)//如果第u位没有选取,那么res的最小值就是 S的集合加上u ,然后从u出发到0的最小值;
res=Math.min(res,rec(S|1<<u,u)+d[v][u]);
}
return dp[S][v]=res;
}
public static void main(String[] args){
Scanner sc=new Scanner(System.in);
n=5;//顶点个数
for (int i = 0; i <100 ; i++) {//初始化
for (int j = 0; j <100 ; j++) {
d[i][j]=INF;
dp[i][j]=-1;
}
}
//存图
d[0][1]=3;
d[0][3]=4;
d[1][2]=5;
d[2][3]=5;
d[2][0]=4;
d[3][4]=3;
d[4][0]=7;
d[4][1]=6;
System.out.println(rec(0,0));
}
}
总结:状态压缩DP,就是把状态用二进制位来表示,用DP求解的一种方法。