HDU3001

HDU3001Travelling

现在有一个具有n个顶点和m条边的无向图(每条边都有一个距离权值),小明可以从任意的顶点出发,他想走过所有的顶点而且要求走的总距离最小,并且他要求过程中走过任何一个点的次数不超过2次。

输入:包含多组实例。每个实例第一行为n(1<=n<=10)和m,接下来m行是对m条边的描述,每行包括a,b(1<=a,b<=n)和c表示节点a和b之间有一条长c的路。

输出:输出他需要走的最短距离,如果不存在这样的路,输出-1.

分析:

   首先本题的图一定要是一个连通的无向图,否则肯定不存在要求的路。且只要是连通的,那么肯定存在要求的路。

如果本题没有说任意一个点走过的次数不超过2这个条件,那么本题可以用类似TSP问题的解法即可,即用d[i][S]=x,表示当前人在i点,并且走过了集合S中的所有点,所行走的最小距离为x。d[i][S]=min{ d[j][S-i]+min_dist[j][i] } j,i属于S。

但是现在要求点出现的次数<=2,所以要把点出现的次数也纳入到d的第二维下标(即S)中去。

   常规我们用S的二进制形式如0101表示点出现的状态含义是:0点出现1次,1点出现0次,2点出现1次,3点出现0次。现在我们需要表示的状态出现次数可能是0,1,2.所以我们应该用S的三进制形式如1201来表示点出现的情况。

   令d[i][S]=x,表示当前人在i点,并且走过了集合S(用S的三进制表示点出现次数的集合)中的所有点以及对应点出现的次数满足S集合时,所行走的最小距离为x。

状态转移方程是:d[i][S]=min{ d[j][S1]+dist[j][i] } ,S与S1的关系是S的三进制形式除了一位减一之外(从2变1或从1变0)其他所有位都与S1的三进制数对应位相同,j在S1集合中出现的次数属于[1,2],i在S1集合中出现的次数属于[0,1],dist[i][j]表示节点i与j之间的初始距离(不是最小距离,如果从i到j没有边则不能执行该状态转移)。初值为:d[i][(3^i)]=0,其他为-1.(本题顶点的标号,题目说是1到n,我们程序中用0到n-1处理。最终我们所求为:max{ d[i][S] },0<=i<=n-1。S的三进制形式中没有一个0(最多2次是通过状态转移方程来限制住的)。

   注意:本题的输入边有重边。

AC代码:2250ms->2140ms(预先用枚举生成所有合法的最终状态)

#include<algorithm>
#include<cstring>
#include<cstdio>
#include<cmath>
using namespace std;
int d[12][60000];//d[i][S]=-1表还没计算过,=1e10表已经计算过但是没有路,>=0表计算过且有路
int n,m;
int dist[15][15];
int t[12];
int cnt;
int state[60000];
bool legal(int S)//判断S的三进制形式中是否没有一个0
{
    bool ok = true;
    for(int j=0;j<=n-1;j++)
    {
        if(S%3==0)
        {
                ok=false;
                break;
        }
        S=S/3;
    }
    return ok;
}
/*
char san[100];
char *print(int S)//将S的三进制形式存入san中,其中S的三进制高位存在san的0位置
{
    for(int j=0;j<=n-1;j++)
    {
        if(S%3)
            san[n-1-j]='0'+S%3;
        else
            san[n-1-j]='0';
        S=S/3;
    }
    san[n]='\0';
    return san;
}
*/
inline bool in(int i,int S)//判断第i个点是否在集合S中出现至少1次
{
    for(int j=0;j<i;j++)
        S = S/3;
    if(S%3)return true;
    return false;
}
int dp(int i,int S)//计算d[i][S]=min{ d[j][S1]+dist[j][i] }
{
    if(d[i][S]>=0)return d[i][S];
    int &ans = d[i][S];
    ans=1e9;
    int S1 = S-t[i];//S1为从S中去除一次i的集合状态
    for(int j=0;j<n;j++)if( j!=i && in(j,S1) && dist[j][i]!=-1 )//j必须在S1中出现至少1次,且j不能为i且(i,j)之间有路
    {
        ans = min(ans,dp(j,S1)+dist[j][i]);
    }
    //printf("d[%d][%s]=%d\n",i,print(S),ans);
    return ans;
}

int main()
{
    int temp=1;
    for(int i=0;i<=10;i++)//用t[i]来保存3的i次幂
    {
        t[i]=temp;
        temp *=3;
    }
    while(scanf("%d%d",&n,&m)==2&&n)
    {
        int max_3=0;//max_3最终的3进制形式为全1111111111
        for(int i=0;i<n;i++)
            max_3 += t[i];

        cnt=0;
        for(int S=max_3;S<t[n];S++)if(legal(S))//S至少每个都出现1次,最多每个都出现两次
        {
            state[cnt++]=S;//预先生成所有的合法状态保存起来
        }

        memset(dist,-1,sizeof(dist));//初始假设所有边都不连通
        for(int i=0;i<m;i++)
        {
            int a,b,c;
            scanf("%d%d%d",&a,&b,&c);
            a--;
            b--;
            if( dist[a][b]==-1 || dist[a][b]>c )
                dist[a][b]=dist[b][a]= c;//保存最小的边,因为有可能有重边
        }
        memset(d,-1,sizeof(d));//初始化表示所有d值还没计算
        for(int i=0;i<n;i++)
            d[i][t[i]] = 0;


        int sum=dp(0,max_3);//sum为所求的最短距离
        for(int i=0;i<n;i++)//当前所在点为 i
        {
            for(int j=0;j<cnt;j++)//从预先保存的所有合法状态中取状态
            {
                sum = min(sum,dp(i,state[j]));
            }
        }
        if(sum==1e9)printf("-1\n");
        else printf("%d\n",sum);
    }
    return 0;
}


AC代码:预处理(预先计算了base[value][p])之后递推的时间是437MS,递归记忆化搜索的时间是625MS。下面是两段代码的合并:

//利用递推做的
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<cmath>
const int INF = 0x3f3f3f3f;//这是一个OJ的极限int数,哪怕+1变成0x3f3f3f40都会WA
using namespace std;
int d[12][60000];//d[i][S]=-1表还没计算过,=1e10表已经计算过但是没有路,>=0表计算过且有路
int n,m;
int dist[15][15];
int cnt;
int t[12];//t[i]=3^i 幂值
int base[60000][15];//base[x][i]=2表数x的三进制表示时第i位是2
int state[60000];
bool legal(int S)//判断S的三进制形式中是否没有一个0
{
    bool ok = true;
    for(int j=0;j<=n-1;j++)
    {
        if(S%3==0)
        {
                ok=false;
                break;
        }
        S=S/3;
    }
    return ok;
}

int main()
{
    int temp=1;
    for(int i=0;i<=10;i++)//用t[i]来保存3的i次幂
    {
        t[i]=temp;
        temp *=3;
    }
    memset(base,0,sizeof(base));
    for(int value=0;value<t[10];value++)//用来预先生base[x][i]=2表示x的三进制第i位是2
    {
        int temp = value;
        int ptr=0;
        while(temp)
        {
            base[value][ptr++]=temp%3;
            temp/=3;
        }
    }
    while(scanf("%d%d",&n,&m)==2&&n)
    {
        int max_3=0;//max_3最终的3进制形式为全1111111111
        for(int i=0;i<n;i++)
            max_3 += t[i];

        cnt=0;
        for(int S=max_3;S<t[n];S++)if(legal(S))//S至少每个都出现1次,最多每个都出现两次
        {
            state[cnt++]=S;//预先生成所有的 [合法] [最终] 状态保存起来
        }

        memset(dist,-1,sizeof(dist));//初始假设所有边都不连通
        for(int i=0;i<m;i++)
        {
            int a,b,c;
            scanf("%d%d%d",&a,&b,&c);
            a--;
            b--;
            if( dist[a][b]==-1 || dist[a][b]>c )
                dist[a][b]=dist[b][a]= c;//保存最小的边,因为有可能有重边
        }
        memset(d,0x3f,sizeof(d));//初始化表示所有d值还没计算
        for(int i=0;i<n;i++)
            d[i][t[i]] = 0;

        for(int S=0;S<t[n];S++)//生成子状态d[j][S],先循环S是因为:
        {                      //要保证计算父状态之前所有可能的子状态都已经计算出来了
            for(int j=0;j<n;j++)
            {
                if(base[S][j]==0)//j不在S中
                    continue;
                for(int i=0;i<n;i++)
                {
                    if(i==j)//前后去到的城市相同
                        continue;
                    if(base[S][i]==2)//集合S已经去过i两次了
                        continue;
                    if(dist[j][i]==-1)//从j到i没有路
                        continue;
                    int now = S+t[i];
                    d[i][now] = min( d[i][now] , d[j][S]+dist[j][i] );
                }
            }
        }
        int sum=INF;//sum为所求的最短距离
        for(int i=0;i<n;i++)//当前所在点为 i
        {
            for(int j=0;j<cnt;j++)//从预先保存的所有合法状态中取状态
            {
                sum = min(sum,d[i][state[j]]);
            }
        }
        if(sum==INF)printf("-1\n");
        else printf("%d\n",sum);
    }
    return 0;
}

//利用记忆化搜索递归
/*
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<cmath>
using namespace std;
int d[12][60000];//d[i][S]=-1表还没计算过,=1e10表已经计算过但是没有路,>=0表计算过且有路
int n,m;
int dist[15][15];
int t[12];
int cnt;
int state[60000];
int base[60000][15];//base[x][i]=2表数x的三进制表示时第i位是2
bool legal(int S)//判断S的三进制形式中是否没有一个0
{
    bool ok = true;
    for(int j=0;j<=n-1;j++)
    {
        if(S%3==0)
        {
                ok=false;
                break;
        }
        S=S/3;
    }
    return ok;
}
inline bool in(int i,int S)//判断第i个点是否在集合S中出现至少1次
{
    for(int j=0;j<i;j++)
        S = S/3;
    if(S%3)return true;
    return false;
}
int dp(int i,int S)//计算d[i][S]=min{ d[j][S1]+dist[j][i] }
{
    if(d[i][S]>=0)return d[i][S];
    int &ans = d[i][S];
    ans=1e9;
    int S1 = S-t[i];//S1为从S中去除一次i的集合状态
    for(int j=0;j<n;j++)
        if( j!=i && base[S1][j]!=0 && dist[j][i]!=-1 )//j必须在S1中出现至少1次,且j不能为i且(i,j)之间有路
        {
        ans = min(ans,dp(j,S1)+dist[j][i]);
        }
    //printf("d[%d][%s]=%d\n",i,print(S),ans);
    return ans;
}

int main()
{
    int temp=1;
    for(int i=0;i<=10;i++)//用t[i]来保存3的i次幂
    {
        t[i]=temp;
        temp *=3;
    }
    memset(base,0,sizeof(base));
    for(int value=0;value<t[10];value++)//用来预先生base[x][i]=2表示x的三进制第i位是2
    {
        int temp = value,p=0;
        while(temp)
        {
            base[value][p++]=temp%3;
            temp/=3;
        }
    }
    while(scanf("%d%d",&n,&m)==2&&n)
    {
        int max_3=0;//max_3最终的3进制形式为全1111111111
        for(int i=0;i<n;i++)
            max_3 += t[i];

        cnt=0;
        for(int S=max_3;S<t[n];S++)if(legal(S))//S至少每个都出现1次,最多每个都出现两次
        {
            state[cnt++]=S;//预先生成所有的合法状态保存起来
        }

        memset(dist,-1,sizeof(dist));//初始假设所有边都不连通
        for(int i=0;i<m;i++)
        {
            int a,b,c;
            scanf("%d%d%d",&a,&b,&c);
            a--;
            b--;
            if( dist[a][b]==-1 || dist[a][b]>c )
                dist[a][b]=dist[b][a]= c;//保存最小的边,因为有可能有重边
        }
        memset(d,-1,sizeof(d));//初始化表示所有d值还没计算
        for(int i=0;i<n;i++)
            d[i][t[i]] = 0;


        int sum=dp(0,max_3);//sum为所求的最短距离
        for(int i=0;i<n;i++)//当前所在点为 i
        {
            for(int j=0;j<cnt;j++)//从预先保存的所有合法状态中取状态
            {
                sum = min(sum,dp(i,state[j]));
            }
        }
        if(sum==1e9)printf("-1\n");
        else printf("%d\n",sum);
    }
    return 0;
}
*/


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值