题意:有n个点(n<=10),m条边的图(可能有重边),问从任一点走遍整个图的最短费用和,其中每个点最多只能经过两次。
思路:tsp问题,状压dp。三进制压缩一个点已被访问的次数作为状态s,dp[s][i]代表状态s下最后一个到达的点位i的最小费用。递推方程:在dp[s][i]已计算出来的情况下,枚举下一个到达的点k,那么dp[news][k] = min{ dp[news][k]+Edge[i][k],dp[news][k] },初始化:dp为INF,dp[3的i次方][i]=0.
注意题目存在重边,因此只保留边长最小的。
两种写法,递推的效率高但顺序相对难掌握,记忆化搜索好些但效率下。
递推:
#include<cstdio>
#include<cstring>
#include<iostream>
#define INF 0x3f3f3f3f
using namespace std;
int n,m;
int tri[12],dig[59050][12];//dig[s][i]表示状态s的第i位
int dp[59050][12];
int Edge[11][11];
int ans;
void init()//初始化计算tri[i]为3的i次方和dig[s][i]
{
tri[0]=1;
for(int i=1;i<=10;++i) tri[i] = tri[i - 1] * 3;
for(int s=0;s<59049;++s)
for(int i=0,t = s;i<10;++i,t/=3) dig[s][i]=t%3;
}
void input()
{
int a,b,c;
memset(Edge,INF,sizeof(Edge));
while(m--)
{
scanf("%d%d%d",&a,&b,&c);
--a,--b;
Edge[b][a]=Edge[a][b]=min(Edge[a][b],c);//存在重边,只记录边长最小的
}
}
void getdp()
{
memset(dp,INF,sizeof(dp));
ans = INF;
for(int i=0;i<n;++i) dp[ tri[i] ][i] = 0;//初始化dp边界:如果s中,仅有i被访问且只被访问一次,费用为0
for(int s=0;s<tri[n];++s)
{
int vall = 1;//计算s是否每个点都访问了一遍
for(int i=0;i<n;++i)
{
if(dig[s][i]==0) vall = 0;
if(dp[s][i]==INF) continue;
for(int k=0;k<n;++k)//枚举i所到的下一个点k
{
if(i == k) continue;
if(Edge[i][k]==INF || dig[s][k]>=2) continue;//如果不存在边<i,k>或者k已被访问2次以上则不进行更新
int news = s + tri[k];//news表示在s的基础上再访问k一次所得状态
dp[news][k] = min(dp[news][k],dp[s][i]+Edge[i][k]);
}
}
if(vall)
for(int i=0;i<n;++i) ans = min(ans,dp[s][i]);//如果每个点都访问过,更新ans
}
}
void getans()
{
printf("%d\n",ans == INF? -1:ans);
}
int main()
{
init();
while(cin>>n>>m)
{
input();
getdp();
getans();
}
return 0;
}
记忆化搜索:
#include<cstdio>
#include<cstring>
#include<iostream>
#define INF 0x3f3f3f3f
using namespace std;
int n,m;
int tri[12],dig[59050][12];
int dp[59050][12];
int Edge[11][11];
int ans;
void init()
{
tri[0]=1;
for(int i=1;i<=10;++i) tri[i] = tri[i - 1] * 3;
for(int s=0;s<59049;++s)
for(int i=0,t = s;i<10;++i,t/=3) dig[s][i]=t%3;
}
void input()
{
int a,b,c;
memset(Edge,INF,sizeof(Edge));
while(m--)
{
scanf("%d%d%d",&a,&b,&c);
--a,--b;
Edge[b][a]=Edge[a][b]=min(Edge[a][b],c);
}
}
int dfs(int s,int i)
{
if(dp[s][i]!=-1) return dp[s][i]; //体现记忆化
int cur = INF;//初始化为INF,如果出现不合法状态会返回INF
if(s==tri[i]) cur = 0;//如果s中,仅有i被访问且只被访问一次
else
if(dig[s][i])//如果i在s中确实被访问了
for(int k=0;k<n;++k)//枚举到i的上一个点k
if(dig[s][k] && Edge[k][i]!=INF && k!=i)//异于i的点k必须在s中被访问过,且存在边<k,i>
cur = min(cur,dfs(s-tri[i],k)+Edge[k][i]);
return dp[s][i] = cur;
}
void getdp()
{
memset(dp,-1,sizeof(dp));
ans = INF;
for(int s=0;s<tri[n];++s)
{
int vall=1;//计算s是否每个点都访问了一遍
for(int i=0;i<n;++i)
{
if(dig[s][i]==0) vall = 0;
dfs(s,i);
}
if(vall) for(int i=0;i<n;++i) ans = min(ans,dp[s][i]);//如果每个点都访问过,更新ans
}
}
void getans()
{
printf("%d\n",ans == INF? -1:ans);
}
int main()
{
init();
while(cin>>n>>m)
{
input();
getdp();
getans();
}
return 0;
}