zoukankan      html  css  js  c++  java
  • 状压dp

    2800 送外卖

     时间限制: 2 s
     空间限制: 256000 KB
     题目等级 : 钻石 Diamond
     
    题目描述 Description

    有一个送外卖的,他手上有n份订单,他要把n份东西,分别送达n个不同的客户的手上。n个不同的客户分别在1~n个编号的城市中。送外卖的从0号城市出发,然后n个城市都要走一次(一个城市可以走多次),最后还要回到0点(他的单位),请问最短时间是多少。现在已知任意两个城市的直接通路的时间。

    输入描述 Input Description

    第一行一个正整数n (1<=n<=15)

    接下来是一个(n+1)*(n+1)的矩阵,矩阵中的数均为不超过10000的正整数。矩阵的i行j列表示第i-1号城市和j-1号城市之间直接通路的时间。当然城市a到城市b的直接通路时间和城市b到城市a的直接通路时间不一定相同,也就是说道路都是单向的。

    输出描述 Output Description

    一个正整数表示最少花费的时间

    样例输入 Sample Input
    3
    0 1 10 10
    1 0 1 2
    10 1 0 10
    10 2 10 0
    样例输出 Sample Output

    8

    数据范围及提示 Data Size & Hint

    1<=n<=15

    分类标签 Tags 点此展开 

    思路:
       首先题目给出了邻接矩阵,而且还说每个点可以访问多次,那么我们考虑两点间的距离便只考虑最短距离,不考虑怎么到的方式和路径 选择Floyd 算法求出两点间的最短距离
       题目数据量较小 先考虑搜索 那么解答树将会是n个节点的全排列 数量达到 n!而且不好剪枝 而且这样想还有一定的问题 题目上说一个点可以经历很多次以达到最短路 那么解答树将不是排列 而是集合的形式 那么搜索就很难解决这道题 并且很明显感到有重叠子问题 那么开始考虑动归

    如果用dp的话 要考虑状态数量和状态的划分和表示 那么由搜索的猜想可以感觉到需要集合 状态可能就要表示为当前集合S中的元素已经全部经历过的最短路径 并且应该记下来集合S中最后一个访问的元素是什么 因为这样的话可以方便的递推出下一个状态最终解决问题 并且题目给出两点最短距离 记下了最后一个访问的点 每一个状态就可以由上一个状态得出 那么状态转移方程

    dp [ 集合S(不包含j) ] [ 要访问的点j ] (表示集合S中的点已经访问,且最后访问的是j的最短路长度)= min { dp [ 已经访问过的集合S' (S'中没有点j,也没有点i) ] [ S'集合中最后一个访问的点是 i ] +dis [ i ] [ j ] }
    其中i属于S枚举(ATT!! i属于S 不属于S' 这里要多理解)就好 就可以开始递推了

       并且问题的答案 就应该在dp[集合S包括除了0的所有点][最后一个访问的是0号点]
     dp的实现:
     首先考虑集合如何表示 如果用数组的话 那么数量是惊人的 因为每个点可以重复 并且无法估计需要的大小 集合 集合有一个性质就是无序性 集合中有 只代表有 没有顺序 这也解释了为什么上文说要记下来最后一个访问的点 那么选择用二进制来表示和压缩状态

    上代码:

    #include<cstdio>
    #include<cstring>
    #include<iostream>
    using namespace std;
    #define N 1<<16
    #define nn 16
    int d[nn][nn],f[N][nn];
    int main () {
        //状态压缩 二进制位表示对应状态 编号n对应的二进制位为1<<n 那一位上是1表示遍历过 是0表示还没有遍历
        /*f[s][i]表示已经遍历的元素在s(二进制数)中 i为最后遍历的编号 则f[s][i]的值表示s中的元素已经遍历完,
        且刚访问完i的最短路长度*/
         int n;//节点数量
        cin>>n;
        for (int i=0; i<=n; i++)
            for (int j=0; j<=n; j++)
                cin>>d[i][j];//输入已经给出的两个节点的距离
        for (int k=0; k<=n; k++)
            for (int i=0; i<=n; i++)
                for (int j=0; j<=n; j++) {
                    if (i!=k&&j!=k&&i!=j) {
                        d[i][j]=min(d[i][j],d[i][k]+d[k][j]);//Floyd 求出i到j的最短路长度记在d[i][j]中
                    }
                }
        int maxx=(1<<(n+1))-1; //最大的状态数 即(1<<(n+1))-1 对应的二进制数(n位)上每一位为1
        for (int i=0; i<N; i++)
            for (int j=0; j<nn; j++)
                f[i][j]=0x6ffffff;//初始化 所有情况的距离初始化为最大值
        for (int i=0; i<=n; i++) f[0][i]=d[0][i];
        /*初始化 s集合中没有点  只刚遍历完i点的最短路值即为d[0][i](0号点到i的最短路) 
        (这是动归的基础起点与边界) 因为题目要求从0号点出发 也是为什么答案在f[集合s有除了0号点的所有点][0]
        代表所有点全部访问完并且最后一个访问的是0号点 符合题目所说从0出发访问再回到0的最短距离*/
        for (int s=1; s<=maxx; s++) //枚举所有状态
            for (int j=0; j<=n; j++) //访问还不在s中的点j 去更新f
                if (!((1<<j)&s)) { //如果j在s里 不访问
                    for (int i=0; i<=n; i++) { //从s中的点(i属于集合s) 出发去更新到j的距离
                        if (s&(1<<i)) { //i在s中
                            f[s][j]=min(f[s][j],f[s^(1<<i)][i]+d[i][j]);
                            //异或即可求出补集(或者叫集合的减法)s^(1<<i) 表示把s集合中的i去掉
                        }
                    }
                }
    ///    int mmax=0x7fffffff;
    //    for(int i=1; i<=n; i++)
    //        mmax=min(mmax, f[maxx^(1<<i)][i]+d[i][0]);
    //    cout<<mmax<<"
    ";
        //如果下面的最终结果不理解 尝试理解下这一种输出 其实两者是一致的一样的 可以帮助你理解下面
        cout<<f[maxx^1][0]; 
        //s集合中有除0号点以外的所有元素,最后一个访问的元素为0号 即为所求
        return 0;
    }
    View Code

    /*-------------------------------------------------*/

    Vijos / 题库 /

    最小总代价

    描述

    n个人在做传递物品的游戏,编号为1-n。

    游戏规则是这样的:开始时物品可以在任意一人手上,他可把物品传递给其他人中的任意一位;下一个人可以传递给未接过物品的任意一人。

    即物品只能经过同一个人一次,而且每次传递过程都有一个代价;不同的人传给不同的人的代价值之间没有联系;
    求当物品经过所有n个人后,整个过程的总代价是多少。

    格式

    输入格式

    第一行为n,表示共有n个人(16>=n>=2);
    以下为n*n的矩阵,第i+1行、第j列表示物品从编号为i的人传递到编号为j的人所花费的代价,特别的有第i+1行、第i列为-1(因为物品不能自己传给自己),其他数据均为正整数(<=10000)。

    (对于50%的数据,n<=11)。

    输出格式

    一个数,为最小的代价总和。

    样例1

    样例输入1

    2
    -1 9794
    2724 –1
    

    样例输出1

    2724
    

    限制

    所有数据时限为1s

    【算法分析】

    看到2<=n<=16,应想到此题和状态压缩dp有关。每个人只能够被传递一次,因此使用一个n位二进制数state来表示每个人是否已经被访问过了。但这还不够,因为从这样的状态中,并不能清楚地知道现在物品在谁 的手中,因此,需要在此基础上再增加一个状态now,表示物品在谁的手上。

    dp[state][now]表示每个人是否被传递的状态是state,物品在now的手上的时候,最小的总代价。

    初始状态为:dp[1<<i][i]=0;表示一开始物品在i手中。

    所求状态为:min(dp[(1<<n)-1][j]); 0<=j<n

    状态转移方程是:

    dp[state][now]=min(dp[pre][t]+dist[now][t]);

    pre表示的是能够到达state这个状态的一个状态,t能够传递物品给now且只有二进制下第t位与state不同。

    状态的大小是O((2n)*n),转移复杂度是O(n)。总的时间复杂度是O((2n)*n*n)。

    上代码:

    #include<cstdio>
    #include<cstring>
    #include<iostream>
    using namespace std;
    
    int n,e[20][20];
    int f[70010][20];//f[state][now]表示每个人是否被传递的状态是state,物品在now的手上的时候,最小的总代价。
    
    int min(int a,int b) {
        if(a==-1) return b;
        if(b==-1) return a;
        return a>b ? b:a;
    }
    
    int main() {
        scanf("%d",&n);
        for(int i=0; i<n; i++)
            for(int j=0; j<n; j++)
                scanf("%d",&e[i][j]);
        memset(f,-1,sizeof(f));
        for(int i=0; i<n; i++) //dp[1<<i][i]=0;表示一开始物品在i手中
            f[1<<i][i]=0;
        int ans=-1;
        for(int i=0; i< 1<<n; i++) //枚举第i个人状态
        for(int j=0; j<n; j++) //第j个人接到 
            if(f[i][j]!=-1)
                for(int k=0; k<n; k++) //第k个人传过来
                    if(!(i & (1<<k) )) {
                        f[i | (1<<k)][k] = min(f[i | (1<<k)][k],f[i][j]+e[j][k]);
                        if((i | (1<<k))==(1<<n)-1) ans=min(ans,f[i|(1<<k)][k]);    
                    }
        if(ans!=-1) printf("%d
    ",ans);
        else printf("0
    ");
        return 0;
    }
    View Code

    自己选的路,跪着也要走完!!!

  • 相关阅读:
    IE8发送ajax请求无效
    Qt 桌面服务 QDesktopServices
    Qt 排序 QSort
    使用ehcache缓存可变对象时的注意事项
    Golang微服务入门到精通之路-1-Go之初体验
    机械革命 Code01 笔记本激活 Win10 方法
    IM技术分享:万人群聊消息投递方案的思考和实践
    零基础入门:基于开源WebRTC,从0到1实现实时音视频聊天功能
    剑指 Offer 28. 对称的二叉树
    idea使用
  • 原文地址:https://www.cnblogs.com/wsdestdq/p/7352743.html
Copyright © 2011-2022 走看看