网上关于集合动态规划的题目好像不多?
关于集合DP的题目基本没见到过多少...但是USACO 2013 Nov. Gold T3居然直接考了个裸的......
于是就怒学一发...
集合DP,意思是我们定义状态的时候使用集合.
一般我们定状态,比如LIS(最长上升子序列),
d[i]: 以第i个元素结尾的最长上升序列长度.
再比如0/1背包,
d[i][j]: 考虑前i件物品,用掉了容量j所能达到的最大价值
或者 d[i][j]: 考虑前i件物品,背包容量上限为j,所能达到的最大价值
适用范围:
1.集合DP是指数级算法,一般有n<=20,将n!的复杂度优化至2^n;
2.集合DP想要求出一个依赖于集合中元素顺序的结果,但是判定方案是否可行或者状态转移不需要得到原集合中元素顺序.
集合dp呢就是"把集合当下标".具体的看下边....
例题在此:
http://www.usaco.org/index.php?page=viewproblem2&cpid=348
题目大意是,给你n<=16个硬币,每个硬币有一个面值c[i],要按照给定先后顺序购买m<=100000个物品,所有物品的价格a[i]由输入给出,不能找零,求最多能留下多少钱(价值).
n<=16是一个很明显的提示.这告诉我们2^n的算法能做.
考虑最优的购买方案.
我们肯定是从n个硬币中选出若干个,然后按照某种排列依次购买物品.
直接枚举硬币的选取情况(某个硬币选或不选),有2^n中可能.
然后我们试着快速地判断这个选取硬币的方案是不是最优的.
怎么评判方案的优劣呢? 显然,剩余的硬币价值之和越大,方案就越好.
对于每种选取硬币的方案S,如果我们能够快速判断它是否能购买完所有物品,那么就能在O(1)时间算出S能带来的最大剩余价值.
于是,我们的任务变成,快速判断S能否购买完所有物品.
于是集合DP登场.
还是枚举S.
注意到他给出的一个小小的条件我们没有用到:物品必须按顺序买,一旦买了物品k,那么物品k-1,物品k-2...物品1都得买.
于是乎...
状态: d[S]: S最多能够买到哪件物品.(S所能取得的最大k)
转移: d[S]=max( d[S\p]+用硬币p在之后的中按顺序购买,最多能买到的物品数 );
读起来比较绕.
我们用S\p购买到了第k=d[S\p]个物品,那么我们接下来就一定要用硬币p购买之后的物品k+1,物品k+2..直到不能买为止.
显然,如果把所有物品买完了,说明S是可行的.最后一个小问题,边界.
边界: d[S]=0,当且仅当S为空(不使用任何硬币).
整个DP就完成了.....
有一个小优化.用硬币p购买物品k+1,物品k+2...直到h-1,h.我们想要快速找出这个h,毕竟m=100000,扫一遍的话速度够呛.
这个直接二分h的位置即可.
接下来是照例帖代码....
直接用map做的记忆化搜索.
最大数据点跑620ms. USACO评测机真心快....
#include <cstdio> #include <iostream> #include <cstdlib> #include <algorithm> #include <map> #include <cstring> #include <fstream> #include <cmath> #include <vector> #include <queue> #include <stack> typedef long long ll; typedef unsigned long long ull; typedef long double db; using namespace std; ifstream fin("nochange.in"); ofstream fout("nochange.out"); int n,m; int a[101000]; int __presum[101000]; int*presum=__presum+1; int c[20]; class Set { public: bool t[17]; Set(){} Set(bool h) { for(int i=0;i<n;i++) t[i]=h; } bool empty() { for(int i=0;i<n;i++) if(t[i]) return false; return true; } bool&operator[](int f) { return t[f]; } Set operator-(int k) { Set c(*this); c[k]=false; return c; } bool operator<(const Set x) const { for(int i=0;i<n;i++) if(x.t[i]!=t[i]) return t[i]<x.t[i]; return false; } }; int res=-1; map<Set,int> g; int DFS(Set x) { if(x.empty()) return 0; if(g.find(x)!=g.end()) return g[x]; int mx=0; for(int i=0;i<n;i++) if(x[i]!=0) { int k=DFS(x-i); mx=max(mx,(int)(upper_bound(presum,presum+m,presum[k-1]+c[i])-presum)); if(mx==m) { int ut=0; for(int i=0;i<n;i++) if(x[i]==0) ut+=c[i]; res=max(ut,res); } } return g[x]=mx; } int main() { fin>>n>>m; for(int i=0;i<n;i++) fin>>c[i]; for(int i=0;i<m;i++) fin>>a[i]; presum[-1]=0; for(int i=0;i<m;i++) presum[i]=presum[i-1]+a[i]; DFS(Set(true)); fout<<res<<endl; return 0; }
位运算优化的代码.最大的数据点跑了60ms.
#include <cstdio> #include <iostream> #include <cstdlib> #include <algorithm> #include <map> #include <cstring> #include <fstream> #include <cmath> #include <vector> #include <queue> #include <stack> typedef long long ll; typedef unsigned long long ull; typedef long double db; using namespace std; ifstream fin("nochange.in"); ofstream fout("nochange.out"); int n,m; int a[101000]; int __presum[101000]; int*presum=__presum+1; int c[20]; int res=-1; int g[145000]; int DFS(int x) { if(x==0) return 0; if(g[x]!=-1) return g[x]; int mx=0; int p=1; for(int i=0;i<n;i++,p<<=1) if((x&p)!=0) { int k=DFS(x-p); mx=max(mx,(int)(upper_bound(presum,presum+m,presum[k-1]+c[i])-presum)); if(mx==m) { int ut=0; int h=1; for(int i=0;i<n;i++,h<<=1) if((x&h)==0) ut+=c[i]; res=max(ut,res); } } return g[x]=mx; } int main() { fin>>n>>m; for(int i=0;i<n;i++) fin>>c[i]; for(int i=0;i<m;i++) fin>>a[i]; presum[-1]=0; for(int i=0;i<m;i++) presum[i]=presum[i-1]+a[i]; int p=0; for(int i=0;i<n;i++) p=p<<1|1; for(int i=0;i<=p;i++) g[i]=-1; DFS(p); fout<<res<<endl; return 0; }
另外USACO2014 Dec. Gold T1 也是集合DP,必须压位,跑了200ms.记得我调了好久才莫名其妙地干掉唯一一个WA点...