背包问题
- 01背包
- 完全背包
- 多重背包
- 混合背包
基础dp
区间dp
- 四边形不等式优化
数位dp
斜率dp
- 基础位运算
- 几个基础例题
背包问题
01背包
是最基础的背包问题,也是很重要的背包,所有有关背包的问题都可一转化为01背包。
上个poj的例题POJ3624
题目大概意思是要在m的容量中求最大价值
如果定义二维数组状态转移方程:dp[i][j]=max(dp[i-1][j-w[i]]+v[i]);
空间复杂度为O(nm),时间复杂度为O(nm);
下面考虑优化
时间复杂度已经没办法但是空间复杂度可以降低,我们考虑dp[i][j]这个状态,dp[i][j]状态是由dp[i-1][j-w[i]]+v[i]和dp[i-1][j]决定的,i是按顺序递增,可以将第一维去掉得到新的转移方程dp[j]=dp[j-w[i]]+v[i];
以后只要是状态i是由状态i-1转移过来的dp都可以将其做成滚动数组节约一维空间
01滚动数组的j必须逆向枚举,例如在计算dp[6]时,dp[6]是由前面的dp[?]转移过来,如果正向枚举dp[?]会被覆盖,所有就只能逆向枚举
#include <cstdio>
#include <algorithm>
using namespace std;
int w[3500],v[3500];
int n,m;
int dp[13000];
int main(){
while(scanf("%d%d",&n,&m)==2){
for(int i=1;i<=n;i++){
scanf("%d%d",&w[i],&v[i]);
}
for(int i=0;i<=m;i++)dp[i]=0;
for(int i=1;i<=n;i++){
for(int j=m;j>=w[i];j--){
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
}
printf("%d\n",dp[m]);
}
return 0;
}
完全背包
完全背包和01背包类似,只是每件物品可以选择多个
HDU1114
如果我们考虑将前i种物品装入j容量的背包转移方程可以写成dp[i][j]=max(dp[i-1][j-kw[i]]+kv[i])
(0<=k<=W/w[i])
时间复杂度为(NW*(sigma(W/w[i])));
空间复杂度为(NW)
下面考虑优化
先是简单优化一下去掉一些决策,若两件物品 i、j 满足 Ci ≤ Cj
且 Wi ≥ Wj,则将可以将物品 j 直接去掉,不用考虑。若一个物品的重量大于W也可以直接去掉,这样能在O(N^2)内实现
转化为 01 背包问题求解
01 背包问题是最基本的背包问题,我们可以考虑把完全背包问题转化为 01 背包问
题来解。
最简单的想法是,考虑到第 i 种物品最多选 ⌊V /Ci⌋ 件,于是可以把第 i 种物品转化为 ⌊V /Ci⌋ 件费用及价值均不变的物品,然后求解这个 01 背包问题。这样的做法完全没有改进时间复杂度,但这种方法也指明了将完全背包问题转化为 01 背包问题的思路:将一种物品拆成多件只能选 0 件或 1 件的 01 背包中的物品。更高效的转化方法是:把第 i 种物品拆成费用为 Ci2^k、价值为 Wi2^k的若干件物
品,其中 k 取遍满足 Ci2k ≤ V 的非负整数。这是二进制的思想。因为,不管最优策略选几件第 i 种物品,其件数写成二进制后,总可以表示成若干个 2k 件物品的和。这样一来就把每种物品拆成 O(log ⌊V /Ci⌋) 件物品,是一个很大的改进。
下面给出滚动数组实现
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int maxn=500+10;
const int inf=0x3f3f3f3f;
int dp[10000+10],w[maxn],v[maxn];
int E,W,F;
int main(){
int T,n;;
scanf("%d",&T);
while(T--){
scanf("%d%d",&E,&F);
W=F-E;
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d%d",&v[i],&w[i]);
}
dp[0]=0;
for(int i=1;i<=W;i++)dp[i]=inf;
for(int i=1;i<=n;i++){
for(int j=w[i];j<=W;j++){
dp[j]=min(dp[j],dp[j-w[i]]+v[i]);
}
}
if(dp[W]<inf)printf("The minimum amount of money in the piggy-bank is %d.\n",dp[W]);
else printf("This is impossible.\n");
}
return 0;
}
多重背包
题目
有 N 种物品和一个容量为 V 的背包。第 i 种物品最多有 Mi 件可用,每件耗费的空间是 Ci,价值是 Wi。求解将哪些物品装入背包可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。
以上面的背包问题进行分析dp[i][j]=max(dp[i-1][j-k*c[i]]+kw[i])(0<=k<=Mi)
时间复杂度为O(V ΣMi);
仍然考虑二进制的思想,我们考虑把第 i 种物品换成若干件物品,使得原问题中第i 种物品可取的每种策略——取 0 . . . Mi 件——均能等价于取若干件代换以后的物品。另外,取超过 Mi 件的策略必能出现。方法是:将第 i 种物品分成若干件 01 背包中的物品,其中每件物品有一个系数。这件物品的费用和价值均是原来的费用和价值乘以这个系数。令这些系数分别为1, 2, 2^2. . . 2^k-1, Mi-2^k + 1,且 k 是满足 Mi-2^k + 1 > 0 的最大整数。例如,如果 Mi为 13,则相应的 k = 3,这种最多取 13 件的物品应被分成系数分别为 1, 2, 4, 6 的四件物品。
这样就将第 i 种物品分成了 O(logMi) 种物品,将原问题转化为了复杂度为O(V ΣlogMi) 的 01 背包问题,是很大的改进。
def MultiplePack(F,C,W,M)
if C · M ≥ V
CompletePack(F,C,W)
return
k ← 1
while k < M
ZeroOnePack(kC,kW)
M ←M - k
k ← 2k
ZeroOnePack(C · M,W · M)
混合背包
问题
如果将前面1、2、3中的三种背包问题混合起来。也就是说,有的物品只可以取一次(01 背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)。应该怎么求解呢?
给出伪码
for i ← 1 to N
if 第 i 件物品属于 01 背包
ZeroOnePack(F,Ci,Wi)
else if 第 i 件物品属于完全背包
CompletePack(F,Ci,Wi)
else if 第 i 件物品属于多重背包
MultiplePack(F,Ci,Wi,Ni)
背包问题还有几个类型,具体参照背包九讲的内容
基础dp
POJ2533
学习线性dp的入门
大概意思是找到一个最长的上升子序列,子序列可以不连续
考虑dp[i]表示前i个元素的最长上升子序列dp[i]=max(dp[j]+1);
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn=10000+10;
int dp[maxn];
int n,a[maxn];
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);dp[i]=1;
}
int ans=0;
for(int i=1;i<=n;i++){
for(int j=i+1;j<=n;j++){
if(a[j]>a[i])dp[j]=max(dp[j],dp[i]+1);
}
ans=max(ans,dp[i]);
}
printf("%d\n",ans);
return 0;
}
考虑dp[i][j]表示在时刻i在位置j上有饼掉落
状态转移方程为dp[i][j]+=max(dp[i+1][j-1],dp[i+1][j],dp[i+1][j+1]);
答案为dp[0][5]
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int maxn=100000+10;
int dp[maxn][15];
int maxx(int a,int b,int c){
int tmp=(a>b?a:b);
int ans=(tmp>c?tmp:c);
return ans;
}
int main(){
int n;
while(scanf("%d",&n)==1&&n){
int t,x,m=0;
memset(dp,0,sizeof(dp));
for(int i=0;i<n;i++){
scanf("%d%d",&x,&t);
dp[t][x]++;
if(t>m)m=t;
}
for(int i=m-1;i>=0;i--){
for(int j=0;j<=10;j++){
dp[i][j]+=maxx(dp[i+1][j-1],dp[i+1][j],dp[i+1][j+1]);
}
}
printf("%d\n",dp[0][5]);
}
return 0;
}
因为从起点的左右两边都可以往下跳,所以我们考虑dp[i][0]表示从平台i的左边跳,dp[i][1]表示从右边跳,转移方程有
dp[i,0] = H[i] - H[m] + Min (dp[m][0] + X1[i] - X1[m], dp[m][1] + X2[m] - X1[i]); m为i左边下面的平台的编号
dp[i,1] = H[i] - H[m] + Min (dp[m][0] + X2[i] - X1[m], dp[m][1] + X2[m] - X2[i]); m为i右边下面的平台的编号
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn=1000+10;
const int inf=0x3f3f3f3f;
struct point{
int l,r,h;
bool operator<(const point& rhs)const{
return h<rhs.h||(h==rhs.h&&l<rhs.l);
}
}p[maxn];
int dp[maxn][2];
int n,x,y,m;
void left(int i){
int k=i-1;
while(k>0&&p[i].h-p[k].h<=m){
if(p[i].l>=p[k].l&&p[i].l<=p[k].r){
dp[i][0]=p[i].h-p[k].h+min(p[i].l-p[k].l+dp[k][0],p[k].r-p[i].l+dp[k][1]);
return;
}
else --k;
}
if(p[i].h-p[k].h>m)dp[i][0]=inf;
else {
dp[i][0]=p[i].h;
}
}
void right(int i){
int k=i-1;
while(k>0&&p[i].h-p[k].h<=m){
if(p[i].r>=p[k].l&&p[i].r<=p[k].r){
dp[i][1]=p[i].h-p[k].h+min(p[k].r-p[i].r+dp[k][1],p[i].r-p[k].l+dp[k][0]);
return ;
}
else --k;
}
if(p[i].h-p[k].h>m)dp[i][1]=inf;
else {
dp[i][1]=p[i].h;
}
}
int solve(){
for(int i=1;i<=n+1;i++){
left(i);right(i);
}
return min(dp[n+1][0],dp[n+1][1]);
}
int main(){
int T;
scanf("%d",&T);
while(T--){
scanf("%d%d%d%d",&n,&x,&y,&m);
for(int i=1;i<=n;i++)scanf("%d%d%d",&p[i].l,&p[i].r,&p[i].h);
p[n+1].l=p[n+1].r=x;
p[n+1].h=y;
p[0].l=-20000;
p[0].r=20000;p[0].h=0;
sort(p,p+n+2);
int ans=solve();
printf("%d\n",ans);
}
return 0;
}
区间dp
区间dp大概思想是由小区间的状态转移到大区间状态,答案一般为dp[1][n]
模板
//mst(dp,0) 初始化DP数组
for(int i=1;i<=n;i++)
{
dp[i][i]=初始值
}
for(int len=2;len<=n;len++) //区间长度
for(int i=1;i+len<=n;i++) //枚举起点
{
int j=i+len-1; //区间终点
for(int k=i;k<j;k++) //枚举分割点,构造状态转移方程
{
dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]+w[i][j]);
}
}
一般的区间dp复杂度为O(n^3),但是减少决策点的范围能优化到O(n*n)
下面说一说四边形不等式优化上个链接就不码字了
https://blog.csdn.net/noiau/article/details/72514812
我们考虑dp[i][j]表示合并区间i…j成一棵树的边长最小
转移方程为dp[i][j]=min(dp[i][k]+dp[k+1][j]+x[k+1]-x[i]+y[j]-y[k])
x,y分别表示坐标数组
#include <cstdio>
#include <cstring>
#include <cmath>
using namespace std;
const int maxn=1000+10;
const int inf=0x3f3f3f3f;
int dp[maxn][maxn],s[maxn][maxn],x[maxn],y[maxn];
int n;
int main(){
while(scanf("%d",&n)!=EOF){
for(int i=1;i<=n;i++)scanf("%d%d",&x[i],&y[i]);
for(int i=1;i<=n;i++){
s[i][i]=i;
dp[i][i]=0;
}
for(int l=1;l<=n;l++){
for(int i=1;i+l<=n;i++){
int j=i+l;
dp[i][j]=inf;
for(int k=s[i][j-1];k<=s[i+1][j];k++){
if(dp[i][k]+dp[k+1][j]+abs(x[k+1]-x[i])+abs(y[k]-y[j])<dp[i][j]){
dp[i][j]=dp[i][k]+dp[k+1][j]+abs(x[k+1]-x[i])+abs(y[k]-y[j]);
s[i][j]=k;
}
}
}
}
printf("%d\n",dp[1][n]);
}
return 0;
}
数位dp
数位dp模板讲解
https://blog.csdn.net/jk211766/article/details/81474632
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
int a[20];
ll dp[20][2];
ll dfs(int pos,int pre,int sta,bool limit)
{
if ( pos==-1 ) return 1;
if ( !limit && dp[pos][sta]!=-1 ) return dp[pos][sta];
int up=limit?a[pos]:9;
ll tmp=0;
for ( int i=0;i<=up;i++ )
{
if ( pre==4 && i==9 ) continue;
tmp+=dfs(pos-1,i,i==4,limit&&i==a[pos]);
}
if ( !limit ) dp[pos][sta]=tmp;
return tmp;
}
ll solve(ll x)
{
int pos=0;
while ( x )
{
a[pos++]=x%10;
x/=10;
}
return dfs(pos-1,-1,0,true);
}
int main()
{
ll l,r,T;
memset(dp,-1,sizeof(dp));
scanf("%lld",&T);
while ( T-- )
{
scanf("%lld",&r);
printf("%lld\n",r-solve(r)+1);
}
return 0;
}
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int dp[20][50000];
int a[20];
int A,B;
int F(int x){
int base=1;
int sum=0;
while(x>0){
sum+=(x%10)*base;
base<<=1;
x/=10;
}
return sum;
}
int dfs(int pos,int num,bool limit){
if(pos<0)return num>=0;
if(num<0)return 0;
if(!limit&&dp[pos][num]!=-1)return dp[pos][num];
int up=limit?a[pos]:9;
int ans=0;
for(int i=0;i<=up;i++){
ans+=dfs(pos-1,num-i*(1<<pos),limit&&i==up);
}
if(!limit)dp[pos][num]=ans;
return ans;
}
int solve(int x, int y)
{
int pos=0;
while(x){
a[pos++]=x%10;
x/=10;
}
return dfs(pos-1,F(y),true);
}
int main(){
int T,kase=0;
scanf("%d",&T);
memset(dp,-1,sizeof(dp));
while(T--){
scanf("%d%d",&A,&B);
printf("Case #%d: %d\n",++kase,solve(B,A));
}
return 0;
}
斜率dp
上个大佬博客
https://blog.csdn.net/bill_yang_2016/article/details/54667902
斜率dp主要是维护一个单调队列,然后进行斜率维护,单调队列讲解链接
https://www.bilibili.com/video/av23189029?from=search&seid=9493230027308586638
我们考虑dp[i]表示前i种的花的最少的钱状态转移为:dp[i]=min((cnt[i]-cnt[j]+10)*p[i]+dp[j])(1<=j<i);
cnt表示前缀和
#include <cstdio>
#include <cstring>
const int N = 110;
int sum[N], val[N], dp[N], que[N];
int n;
void init() {
dp[0] = sum[0] = 0;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d%d", &sum[i], &val[i]);
sum[i] += sum[i - 1];
}
}
int getUp(int j, int k) {
return dp[j] - dp[k];
}
int getDown(int j, int k) {
return sum[j] - sum[k];
}
int getDp(int i, int j) {
return dp[j] + (sum[i] - sum[j] + 10) * val[i];
}
void solve(){
int head, tail;
head = tail = 0;
que[tail++] = 0;
for (int i = 1; i <= n; i++) {
while (head + 1 < tail && getUp(que[head + 1], que[head]) <= getDown(que[head + 1], que[head]) * val[i])
head++;
dp[i] = getDp(i, que[head]);
while (head + 1 < tail && getUp(i, que[tail - 1]) * getDown(que[tail - 1], que[tail - 2]) <= getUp(que[tail - 1], que[tail - 2]) * getDown(i, que[tail - 1]))
tail--;
que[tail++] = i;
}
printf("%d\n", dp[n]);
}
int main() {
int test;
scanf("%d", &test);
while (test--) {
init();
solve();
}
return 0;
}
dp问题还有最后一种状压dp,也是最难的一种dp以后有机会再写