状压dp

Posted 橘生淮南终洛枳

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了状压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<<"\n";
    //如果下面的最终结果不理解 尝试理解下这一种输出 其实两者是一致的一样的 可以帮助你理解下面
    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\n",ans);
    else printf("0\n");
    return 0;
}
View Code

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

以上是关于状压dp的主要内容,如果未能解决你的问题,请参考以下文章

POJ1699 Best Sequence(AC自动机+状压DP)

状压DP之初尝插头DP

集合划分(状压DP)

dp-状压dp

动态规划---状压dp

种植方案(状压dp)