题目描述
某乡有n个村庄( 1 < n ≤ 20 1<n\le20 1<n≤20 ),有一个售货员,他要到各个村庄去售货,各村庄之间的路程 s ( 0 < s < 1000 ) s(0<s<1000) s(0<s<1000)是已知的,且A村到B村与B村到A村的路大多不同。为了提高效率,他从商店出发到每个村庄一次,然后返回商店所在的村,假设商店所在的村庄为1,他不知道选择什么样的路线才能使所走的路程最短。请你帮他选择一条最短的路。
输入格式:
村庄数n和各村之间的路程(均是整数)。
输出格式:
最短的路程。
说明
输入解释
3 {村庄数}
0 2 1 {村庄1到各村的路程}
1 0 2 {村庄2到各村的路程}
2 1 0 {村庄3到各村的路程}
题目分析
TSP问题,也算是状压DP经典吧
我们以一串二进制数表示村庄的集合(状态)
1表示该村庄访问过,0表示没有
d p [ i ] [ j ] dp[i][j] dp[i][j]表示从起点到第j号点,且到达时状态恰好为i的最短路
则最后答案就是
m
i
n
(
d
p
[
(
1
<
<
n
)
−
1
]
[
i
]
+
m
a
p
[
i
]
[
1
]
)
min(dp[(1<< n) -1][i] + map[i][1])
min(dp[(1<<n)−1][i]+map[i][1])
(
2
<
=
i
<
=
n
)
(2<=i<=n)
(2<=i<=n)
其中map数组是题目给定的各村庄间距离
而求取dp数组的方法,我们可以借鉴Floyd的思想
具体代码:
for(int i=0;i<=(1<<n)-1;++i)
for(int j=1;j<=n;++j)
if( ( (1<<j-1)&i )==0 )
for(int k=1;k<=n;++k)
if( ( (1<<k-1)&i) )
dp[i|(1<<j-1)][j]=min(dp[i|(1<<j-1)][j],dp[i][k] + rem[k][j]);
如何解释呢
第一层循环 i 枚举每个状态
第二层循环 j 枚举下一步到达的点
if( !( (1 << j-1) & i) ) 这句判断 j 是否已访问
1左移j-1位,则此时只有第j位是1
若状态 i 的第 j 位为1,则&与运算返回1,表示已访问,否则没访问
第三层循环枚举中介点k,
其中if语句判断同上
d
p
[
i
∣
(
1
<
<
j
−
1
)
]
[
j
]
=
m
i
n
(
d
p
[
i
∣
(
1
<
<
j
−
1
)
]
[
j
]
,
d
p
[
i
]
[
k
]
+
r
e
m
[
k
]
[
j
]
)
;
dp[i|(1<<j-1)][j]=min(dp[i|(1<<j-1)][j],dp[i][k] + rem[k][j]);
dp[i∣(1<<j−1)][j]=min(dp[i∣(1<<j−1)][j],dp[i][k]+rem[k][j]);
i
∣
(
1
<
<
j
−
1
)
i|(1<<j-1)
i∣(1<<j−1) 将状态
i
i
i的第
j
j
j为置为1得到下一步的状态
d
p
[
i
]
[
k
]
+
m
a
p
[
k
]
[
j
]
dp[i][k] + map[k][j]
dp[i][k]+map[k][j]表示在当前状态i中寻找中介点检查最短路是否可以更新
#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
#include<queue>
#include<cstring>
using namespace std;
int read()
{
int f=1,x=0;
char ss=getchar();
while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
return f*x;
}
const int maxn=1100010;
int n,ans=2e9;
int dp[maxn][25];
int rem[25][25];
int main()
{
n=read();
for(int i=1;i<=n;++i)
for(int j=1;j<=n;++j)
rem[i][j]=read();
memset(dp,67,sizeof(dp));
dp[1][1]=0;//状态1表示此时只有1号点访问过
for(int i=0;i<=(1<<n)-1;++i)//dp过程解释如上
for(int j=1;j<=n;++j)
if( ( (1<<j-1)&i )==0 )
for(int k=1;k<=n;++k)
if( ( (1<<k-1)&i) )
dp[i|(1<<j-1)][j]=min(dp[i|(1<<j-1)][j],dp[i][k] + rem[k][j]);
for(int i=2;i<=n;i++)//最后从状态(1<<n)-1(二进制全为1)中寻找到1最短的点
ans=min(ans,dp[(1<<n)-1][i] + rem[i][1]);
printf("%lld",ans);
return 0;
}
n<=20的数据范围在状压题中算是开到极限了,蒟蒻担心卡常所以开了O2,最大的点跑了541ms,目测不开O2也是能过的
9.15——Update
再提供一种状压搜索的思路
虽然试了好几次都卡不过最后一个点
所以仅供参考学习吧,因为这种思路也挺常用的
dp数组含义依然同上,
初始
d
p
[
1
]
[
1
]
=
0
dp[1][1]=0
dp[1][1]=0
搜索具体代码,应该不难理解
void dfs(int x,int u)//x是当前状态,u是当前到达的结点
{
for(int i=1;i<=n;++i)
if(((1<<i-1)&x)==0)//枚举还未到达的点
if(dp[x|(1<<i-1)][i]>dp[x][u]+rem[u][i])//如果可以更新就继续搜索
{
dp[x|(1<<i-1)][i]=dp[x][u]+rem[u][i];
dfs(x|(1<<i-1),i);
}
}
最终答案判断方法与上述DP相同
虽然这种方法艹不过此题,但建议要理解这种思路
#include<iostream>
#include<cmath>
#include<algorithm>
#include<queue>
#include<cstring>
#include<cstdio>
using namespace std;
typedef double dd;
int read()
{
int f=1,x=0;
char ss=getchar();
while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
return f*x;
}
const int maxn=1050000;
int n,ans=2e9;
int rem[22][22],dp[maxn][22];
void dfs(int x,int u)
{
for(int i=1;i<=n;++i)
if(((1<<i-1)&x)==0)
if(dp[x|(1<<i-1)][i]>dp[x][u]+rem[u][i])
{
dp[x|(1<<i-1)][i]=dp[x][u]+rem[u][i];
dfs(x|(1<<i-1),i);
}
}
int main()
{
n=read();
for(int i=1;i<=n;++i)
for(int j=1;j<=n;++j)
rem[i][j]=read();
memset(dp,67,sizeof(dp)); dp[1][1]=0;
dfs(1,1);
for(int i=1;i<=n;++i)
ans=min(ans,dp[(1<<n)-1][i]+rem[i][1]);
printf("%d",ans);
return 0;
}