Poj3311

Poj3311题解

题目链接

题意

一个人从固定起点(0)出发,经过其余各点(可重复经过一个点),最终回到起点,求最短路程。

笺释

首先,这道题为什么不是一个纯粹图论问题?DP的立足点是状态,由一个状态单调转移到另一个状态,而图论的立足点是图,是点与边。我们无论是从点切入还是边切入,实际上都无法保证从起点出发最终回到起点。
其次,这道题和vijos1456很像,区别在于以下两点
* 那道题从任意起点出发,经过其余各点后回到任意终点即可
* 那道题不允许重复经过同一个点,而该题允许重复经过

而对于vijos1456这道题,因为他要求不能重复经过同一个点,这个要求也不是任何一个最小生成树的算法能满足的(何况这是有向图)。我们固然可以使用dfs对已经访问过的点标记vis穷举所有情况,但那样会超时。
综上所述,无论是vijos1456还是poj3311,都只有采用dp记录状态才能满足题目的要求。这也反映了这样一个问题“两个极为相似的问题,但即使是一点小小的改动,也会引起算法巨大改变”
回到这道题。
因为之前做过类似的vijos1456,所以这道题首先想到的思路还是枚举起点和状态作为一个
DP对象然后利用这个状态更新之后的状态。但是遇到了重复访问已访问过点的问题.
在之前的代码中,我实际上是利用“在已知状态i中加入点k,若点k已经包含在已知状态i中,则dp[j][i]+a[j][k]**一定大于**dp[k][i|1《《k]来保证的一个点k不重复访问。(因为此时i等于k,设dp[j][i]为x 不等式可写作x+a>=x)

for(int i=0;i<=(1<<n)-1;i++)
    {
        for(int j=0;j<=n-1;j++)
        {
            for(int k=0;k<=n-1;k++)
            {
                if(j!=k)
                {
                if(dp[j][i]+a[j][k]<dp[k][i|1<<k])
                {
                    dp[k][i|1<<k]=dp[j][i]+a[j][k];
                }
                }
            }
        }
    }

但是这道题恰好要求我们可以重复访问,以已知状态i+起点**j为一个**DP对象,更新的还是终点k与i|1《《k这个新的DP对象,不同之处在于我选择了枚举所有中间点l,这个思想实际上是从agc022_C中学到的,通过枚举中间点l,我们就可以满足题目重复访问已经访问过的点的要求。
还有这样一个问题困扰了我很久:我们既然访问了中间点l,其实质是经过了由起点j到l的一条路径,又经过l到k的一条路径,最终到达了k点,那么这条最短路径上所有点是否都应在我们要更新的状态中表示出来呢?
这个问题可以通过这样的分类讨论得到疏解
* 若路径上任意一点x在已知状态i中属于已经访问过的点,我们不用标记x,因为x已经在i中得到了标记。
* 若路径任意一点x在已知状态i中不属于已经访问过的点,我们也不用标记x,因为如果x不属于已经访问过的点,我们实际上就没有面临有可能访问已经访问过的点这个困境,我们也就没有重复访问一个点,换句话说,这种状态一定包含在之后会更新到的状态中,更深一步说,这种情况:作为起点的j->作为中间点的x->作为终点的k实质上等价于
1 起点j->作为终点的x
2 起点x->作为终点的k。
这两步更新所得到的结果。

  for(int i=1;i<=(1<<(n+1))-1;i++)
        {
            for(int j=0;j<=n;j++)
            {
                for(int k=0;k<=n;k++)
                {
                    for(int l=0;l<=n;l++)
                    {
                        if(dp[j][i]!=INF)
                        {
                          //  printf("A %d %d %d\n",j,i,l);
                            //此时一定在j 而l可能访问过可能没有
                            //但不管怎么说 都必须从j到l再到k 则l和k都算是访问过了
                            if(dp[j][i]+dis[j][l]+dis[l][k]<dp[k][i|1<<k])
                            {
                                dp[k][i|1<<k]=dp[j][i]+dis[j][l]+dis[l][k];
                            }
                        }
                    }
                }
            }
        }

A掉之后,在网上看了一下别人的题解,发现是一道典型的旅行商问题(第一次听说QAQ),写一下对别人题解的理解。

for(int S = 0;S <= (1<<n)-1;++S)//枚举所有状态,用位运算表示
            for(int i = 1;i <= n;++i)
            {
                if(S & (1<<(i-1)))//状态S中已经过城市i
                {
                    if(S == (1<<(i-1))) dp[S][i] = dis[0][i];//状态S只经过城市I,最优解自然是从0出发到i的dis,这也是DP的边界
                    else//如果S有经过多个城市
                    {
                        dp[S][i] = INF;
                        for(int j = 1;j <= n;++j)
                        {
                            if(S & (1<<(j-1)) && j != i)//枚举不是城市I的其他城市
                                dp[S][i] = min(dp[S^(1<<(i-1))][j] + dis[j][i],dp[S][i]);
                            //在没经过城市I的状态中,寻找合适的中间点J使得距离更短,和FLOYD一样
                        }
                    }
                }
            }

这一段代码是@chinaczy的一段极其优雅的代码,他的思路应该是这样的:
* 由一个出发城市i和状态S确定一个DP对象, 这个DP对象是由这两种情况得到的

  1. 这个状态只包含这一个城市i,那么最短到达方式一定是从起点直接到达(注意不是狭义上的由起点0直接走到i而是由已经预处理得到的最短路径0->i)
  2. 2.这个状态包含多个城市i1,i2,i3…iN,那么既然此时在i城市,之前就一定是从城市集合A中减去城市i剩余的城市中得到的i(同样是广义最短路径),于是我们可以枚举去掉i之后在剩余的城市完成转移 。

他的思路应该是填表法,我的思路是填表法,填表法一般比较好想,但这次应该是比较难转移了。

Essence

通过这道题,我理解到了这样的编程技巧:
* 在考虑一个算法的时候我们应当借助于模拟帮助理解,应当利用考虑这种算法是否穷尽了所有可能的可能性,但也有一点同样重要:我们必须从这个算法的本身数据结构与描述对象取思考算法,而不是硬要用他去套模拟与可能性

完整代码

#include<cstdio>
#include<cstring>
#include<queue>
#define INF 0x3f3f3f3f
#define MAXN 12
using namespace std;
int n,tem,dis[MAXN][MAXN],nums,head[MAXN],vis[MAXN],dp[MAXN][1<<13],ans=INF;
struct edge
{
    int next,to,dis;
}edge[105];
void addedge(int from,int to,int dis)
{
    edge[++nums].next=head[from];
    edge[nums].to=to;
    edge[nums].dis=dis;
    head[from]=nums;
}
void spfa(int s)
{
    queue<int>q;
    for(int i=0;i<=n;i++)
    {
        dis[s][i]=INF;
        vis[i]=0;
}
    q.push(s);dis[s][s]=0;vis[s]=1;
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        vis[u]=0;
        for(int i=head[u];i;i=edge[i].next)
        {
            int v=edge[i].to;
            if(dis[s][v]>dis[s][u]+edge[i].dis)
            {
                dis[s][v]=dis[s][u]+edge[i].dis;
                if(vis[v]==0)
                {
                    vis[v]=1;
                    q.push(v);
                }
            }
        }
    }
}
int main()
{
    while(scanf("%d",&n),n)
    {
        memset(dis,0,sizeof(dis));
        memset(head,0,sizeof(head));
        memset(vis,0,sizeof(vis));
        for(int i=0;i<=n;i++)
        {
            for(int j=0;j<=n;j++)
            {
                scanf("%d",&tem);
                addedge(i,j,tem);
            }
        }
        for(int i=0;i<=n;i++)
        {
            spfa(i);
        }
        memset(dp,0x3f,sizeof(dp));
        dp[0][1]=0;
        //我们的假设是从一个状态0一直更新有更多1的状态 要让每一个状态都实现最大程度的更新 则最终的状态11111也就是所有可能性的结果
        //不能一直从模拟的视角看问题 要从数据结构的视角看问题
        //我们有此时所在的位置 有此时的状态 就可以确定下一次能够到达的位置与更新的状态
        //我们也只能确定这些
        //为了让每一个状态实现最大程度的更新 比如说状态1 11(起点是1 已经更新过0和1)  如果已经访问过的点不能再访问 我们可以更新其他状态111 1011 10011等
     //   printf("E %d\n",(1<<(n+1))-1);
        for(int i=1;i<=(1<<(n+1))-1;i++)
        {
            for(int j=0;j<=n;j++)
            {
                for(int k=0;k<=n;k++)
                {
                    for(int l=0;l<=n;l++)
                    {
                        if(dp[j][i]!=INF)
                        {
                          //  printf("A %d %d %d\n",j,i,l);
                            //此时一定在j 而l可能访问过可能没有
                            //但不管怎么说 都必须从j到l再到k 则l和k都算是访问过了
                            if(dp[j][i]+dis[j][l]+dis[l][k]<dp[k][i|1<<k])
                            {
                             //   printf("D J%d I%d L%d DP[J][I]%d DIS[J][K]%d DIS[L][K]%d\n",j,i,l,dp[j][i],dis[j][k],dis[l][k]);
                               // printf("B %d %d %d\n",k,i|1<<k,dp[k][i|1<<k]);
                                dp[k][i|1<<k]=dp[j][i]+dis[j][l]+dis[l][k];
                               // printf("C %d %d %d\n",k,i|1<<k,dp[k][i|1<<k]);
                            }
                        }
                    }
                }
            }
        }
                printf("%d\n",dp[0][(1<<(n+1))-1]);
    }
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值