状压dp
Posted 橘生淮南终洛枳
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了状压dp相关的知识,希望对你有一定的参考价值。
有一个送外卖的,他手上有n份订单,他要把n份东西,分别送达n个不同的客户的手上。n个不同的客户分别在1~n个编号的城市中。送外卖的从0号城市出发,然后n个城市都要走一次(一个城市可以走多次),最后还要回到0点(他的单位),请问最短时间是多少。现在已知任意两个城市的直接通路的时间。
第一行一个正整数n (1<=n<=15)
接下来是一个(n+1)*(n+1)的矩阵,矩阵中的数均为不超过10000的正整数。矩阵的i行j列表示第i-1号城市和j-1号城市之间直接通路的时间。当然城市a到城市b的直接通路时间和城市b到城市a的直接通路时间不一定相同,也就是说道路都是单向的。
一个正整数表示最少花费的时间
3 0 1 10 10 1 0 1 2 10 1 0 10 10 2 10 0
8
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; }
/*-------------------------------------------------*/
最小总代价
描述
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; }
自己选的路,跪着也要走完!!!
以上是关于状压dp的主要内容,如果未能解决你的问题,请参考以下文章