动态规划攻略

数字三角形模型

方格取数

题目

设有 N×N 的方格图,我们在其中的某些方格中填入正整数,而其它的方格中则放入数字0。如下图所示:

2.gif

某人从图中的左上角 A 出发,可以向下行走,也可以向右行走,直到到达右下角的 B 点。

在走过的路上,他可以取走方格中的数(取走后的方格中将变为数字0)。

此人从 A 点到 B 点共走了两次,试找出两条这样的路径,使得取得的数字和为最大。

思路

为什么不能分开走:第一次走到(n, n)求出最大值并记录路径令路径上点收益为0后再走一次。而第一次走过的地方会被重置成0,你不确定的是第一次同样是最优解而未被你选择的情况下第二次的值会不会比你已经得出的答案要大。

代码

/*
优化一下可发现,因为每步同时走 i1 + j1 == i2 + j2 必然成立
可令k = i1 + j1
则dp[i1][j1][i2][j2] 可转化为 dp[k][i1][i2]
*/
#include<bits/stdc++.h>
using namespace std;
const int N=11;
int mp[N][N],dp[N<<1][N][N];		//表示[总步数][i1的值][i2的值]
int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    int n,u,v,w;
    cin>>n;
    while(cin>>u>>v>>w,u||v||w)mp[u][v]=w;
    for(int k=2;k<=n*2;k++){
        for(int i1=1;i1<=n;i1++){
            int j1=k-i1;
            if(j1<1||j1>n)continue;
            for(int i2=1;i2<=n;i2++){
                int j2=k-i2;
                if(j2<1||j2>n)continue;
                int &x=dp[k][i1][i2];
                x=dp[k-1][i1][i2];
                x=max(x,dp[k-1][i1-1][i2]);
                x=max(x,dp[k-1][i1][i2-1]);
                x=max(x,dp[k-1][i1-1][i2-1]);
                x+=mp[i1][j1];
                if(i1!=i2)x+=mp[i2][j2];
            }
        }
    }
    cout<<dp[n*2][n][n]<<'\n';
    return 0;
}

传纸条

题目

班里每个同学都可以帮他们传递,但只会帮他们一次,也就是说如果此人在小渊递给小轩纸条的时候帮忙,那么在小轩递给小渊的时候就不会再帮忙,反之亦然。

还有一件事情需要注意,全班每个同学愿意帮忙的好感度有高有低(注意:小渊和小轩的好心程度没有定义,输入时用 0 表示),可以用一个 0∼1000 的自然数来表示,数越大表示越好心。

小渊和小轩希望尽可能找好心程度高的同学来帮忙传纸条,即找到来回两条传递路径,使得这两条路径上同学的好心程度之和最大。

现在,请你帮助小渊和小轩找到这样的两条路径。

思路

传纸条为何可以使用方格取数的代码:为走一个点所得的值一定比一个走这个点另一个走另一个点得到的好心程度之和小,所以不会有交点

代码

#include<bits/stdc++.h>
using namespace std;
const int N=55;
int mp[N][N],dp[N<<1][N][N];
int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            cin>>mp[i][j];
    for(int k=2;k<=n+m;k++){
        for(int i1=1;i1<=n;i1++){
            int j1=k-i1;
            if(j1<1||j1>m)continue;
            for(int i2=1;i2<=n;i2++){
                int j2=k-i2;
                if(j2<1||j2>m)continue;
                int &x=dp[k][i1][i2];
                x=dp[k-1][i1][i2];
                x=max(x,dp[k-1][i1-1][i2]);
                x=max(x,dp[k-1][i1][i2-1]);
                x=max(x,dp[k-1][i1-1][i2-1]);
                x+=mp[i1][j1];
                if(i1!=i2)x+=mp[i2][j2];
            }
        }
    }
    cout<<dp[n+m][n][n]<<'\n';
    return 0;
}

最长上升子序列模型

最长上升子序列

题目

给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。(数据范围:1≤N≤1000)

思路

代码

#include<bits/stdc++.h>
using namespace std;
const int mxn=1e3+5,inf=0xc0c0c0c0;
int a[mxn],dp[mxn],n,ans;
int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i];
    for(int i=1;i<=n;i++){
        dp[i]=1;
        for(int j=1;j<i;j++){
            if(a[i]>a[j])dp[i]=max(dp[i],dp[j]+1);
        //  if(a[i]<a[j])dp[i]=max(dp[i],dp[j]+1);	下降
        }
        ans=max(ans,dp[i]); 
    }
    cout<<ans;
    return 0;
}

最长上升子序列 II

题目

给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。(数据范围:1≤N≤100000)

思路

用 a 数组存储上升子序列,tot记录数组数量,输入一个数 x 如果大于a[tot],则a[++tot]=x,否则在数组中找出最后一个比这个数小的数据,替换掉它后一个数,因为最前面的数据有可能被换的更小,这样上升的空间更大。

代码

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int a[N];
int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    int n,tot=1;
    cin>>n>>a[1];
    for(int i=1;i<n;i++){
        int x;
        cin>>x;
        if(x>a[tot])a[++tot]=x;
        else{
            int m=lower_bound(a+1,a+1+tot,x)-a;
            a[m]=x;
        }
    }
    cout<<tot;
    return 0;
}

登山

题目

五一到了,PKU-ACM队组织大家去登山观光,队员们发现山上一个有N个景点,并且决定按照顺序来浏览这些景点,即每次所浏览景点的编号都要大于前一个浏览景点的编号。同时队员们还有另一个登山习惯,就是不连续浏览海拔相同的两个景点,并且一旦开始下山,就不再向上走了。队员们希望在满足上面条件的同时,尽可能多的浏览景点,你能帮他们找出最多可能浏览的景点数么?

思路

求以某一点划分的前面区间的最长上升子序列和后面区间最长下降子序列。

代码

#include<bits/stdc++.h>
using namespace std;
const int mxn=1e3+5;
int a[mxn],b[mxn],c[mxn],n;
int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i];
    for(int i=1;i<=n;i++){    //求到i的最长上升子序列
        c[i]=1;
        for(int j=1;j<i;j++){
            if(a[i]>a[j])c[i]=max(c[i],c[j]+1);
        }
    }
    for(int i=n;i>0;i--){    //求以i为起点坐标的最长下降子序列
        b[i]=1;
        for(int j=n;j>i;j--){
            if(a[i]>a[j])b[i]=max(b[i],b[j]+1);
        }
    }
    int ans=0;
    for(int i=1;i<=n;i++)ans=max(ans,b[i]+c[i]-1);
    cout<<ans;
    return 0;
}

当前状态为上升状态,则下一状态可以是上升状态也可以是下降状态

当前状态为下降状态,则下一状态只能是下降状态

#include<bits/stdc++.h>
using namespace std;
const int mxn=1e3+5;
int dp[mxn][2];    //0记录上升,1记录下降
int a[mxn],n;
int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i];
    int ans=0;
    for(int i=1;i<=n;i++){
        dp[i][0]=dp[i][1]=1;
        for(int j=1;j<i;j++){
            if(a[i]>a[j])dp[i][0]=max(dp[i][0],dp[j][0]+1);
            if(a[i]<a[j])dp[i][1]=max(dp[i][1],max(dp[j][0],dp[j][1])+1);
        }
        ans=max(ans,max(dp[i][0],dp[i][1]));
    }
    cout<<ans;
    return 0;
}

友好城市

题目

北岸的每个城市有且仅有一个友好城市在南岸,而且不同城市的友好城市不相同。

每对友好城市都向政府申请在河上开辟一条直线航道连接两个城市,但是由于河上雾太大,政府决定避免任意两条航道交叉,以避免事故。

编程帮助政府做出一些批准和拒绝申请的决定,使得在保证任意两条航线不相交的情况下,被批准的申请尽量多。

思路

南岸坐标按从小到大排序,北岸则找最长上升子序列。

代码

#include<bits/stdc++.h>
using namespace std;
const int mxn=5e3+5;
struct node{
    int l,r;
    bool operator<(const node&b)const{
        return l<b.l;
    }
}nb[mxn];
int n,dp[mxn];
int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)cin>>nb[i].l>>nb[i].r;
    sort(nb+1,nb+1+n);
    int ans=0;
    for(int i=1;i<=n;i++){
        dp[i]=1;
        for(int j=1;j<i;j++){
            if(nb[i].r>nb[j].r)dp[i]=max(dp[i],dp[j]+1);
        }
        ans=max(ans,dp[i]);
    }
    cout<<ans;
    return 0;
}

拦截导弹

题目

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。

但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。

某天,雷达捕捉到敌国的导弹来袭。

由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。

输入导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数,导弹数不超过1000),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

题目

能拦截多少导弹求最长不上升子序列

要配备多少套这种导弹拦截系统求最长上升子序列

代码

#include<bits/stdc++.h>
using namespace std;
const int mxn=1e3+5;
int a[mxn],b[mxn],c[mxn],n;
int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    while(cin>>a[++n]);
    int ans=0,res=0;
    for(int i=1;i<n;i++){
        b[i]=c[i]=1;
        for(int j=1;j<i;j++){
            if(a[i]<=a[j])c[i]=max(c[i],c[j]+1);
            else b[i]=max(b[i],b[j]+1);
        }
        ans=max(ans,b[i]);
        res=max(res,c[i]);
    }
    cout<<res<<'\n'<<ans;
    return 0;
}

背包模型

01背包问题

题目

N件物品和容量为V的背包,每件物品体积v,价值w,每件物品只可以存一次,求不超过背包容量价值最大多少。

解体思路:状态dp [ i ] [ j ] [i][j] [i][j]表示前i的物体背包容量为j下的最大价值。

思路

书包重量为 j 的最大价值是书包为 j - v 的最大价值+w。

代码

#include<bits/stdc++.h>
using namespace std;
const int mxn=1e3+5;
int n,m,v,w,dp[mxn][mxn];
int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>v>>w;
        for(int j=1;j<=m;j++){
            if(j<v)dp[i][j]=dp[i-1][j];
            else dp[i][j]=max(dp[i-1][j],dp[i-1][j-v]+w);
        }
    }
    cout<<dp[n][m];
    return 0;
}

一维做法

#include<bits/stdc++.h>
using namespace std;
const int mxn=1e3+5;
int n,m,v,w,dp[mxn];
int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>v>>w;
        for(int j=m;j>=v;j--){
            dp[j]=max(dp[j],dp[j-v]+w);
        }
    }
    cout<<dp[m];
    return 0;
}

完全背包问题

题目

N件物品和容量为V的背包,每件物品体积v,价值w,每件物品可以存无限次,求不超过背包容量价值最大多少。

思路

代码

#include<bits/stdc++.h>
using namespace std;
const int mxn=1e3+5;
int n,m,v,w,dp[mxn];
int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>v>>w;
        for(int j=1;j<=m;j++){
            if(j<v)dp[j]=dp[j];
            else dp[j]=max(dp[j],dp[j-v]+w);
        }
    }
    cout<<dp[m];
    return 0;
}

多重背包问题 I

题目

N件物品和容量为V的背包,每件物品体积v,价值w,每件物品可以存s次,求不超过背包容量价值最大多少。

思路

01背包解法

代码

#include<bits/stdc++.h>
using namespace std;
const int mxn=1e3+5;
int n,m,v,w,s,dp[mxn];
int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>v>>w>>s;
        while(s--){
            for(int j=m;j>0;j--){
                if(j<v)dp[j]=dp[j];
                else dp[j]=max(dp[j],dp[j-v]+w);
            }
        }
    }
    cout<<dp[m];
    return 0;
}

多重背包问题 II

题目

有 N 种物品和一个容量是 V 的背包。

第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

思路

二进制优化

代码

#include<bits/stdc++.h>
using namespace std;
const int mxn=12005,mxm=2e3+5;
int n,m,a,b,s,num;
int v[mxn],w[mxn],dp[mxm];
int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>a>>b>>s;
        for(int j=1;j<=s;j<<=1){
            v[++num]=a*j,w[num]=b*j;
            s-=j;
        }
        if(s)v[++num]=a*s,w[num]=b*s;
    }
    for(int i=1;i<=num;i++){
        for(int j=m;j>=v[i];j--){
            dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
        }
    }
    cout<<dp[m];
    return 0;
}

多重背包问题 III

题目

有 N 种物品和一个容量是 V 的背包。

第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

思路

单调队列优化

dp[j] = dp[j]
dp[j+v] = max(dp[j], dp[j+v] - w) + w
dp[j+2v] = max(dp[j], dp[j+v] - w, dp[j+2v] - 2w) + 2w
dp[j+3v] = max(dp[j], dp[j+v] - w, dp[j+2v] - 2w, dp[j+3v] - 3w) + 3w

每次入队的值是 dp[j + k * v] - k * w

代码

#include<bits/stdc++.h>
using namespace std;
const int mxn=2e4+5;
int n,m,v,w,s;
int q[mxn],g[mxn],f[mxn];
int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>v>>w>>s;
        memcpy(g,f,sizeof f);
        for(int j=0;j<v;j++){
            int hh=0,tt=-1;
            for(int k=j;k<=m;k+=v){
                //如果队列元素个数超过s个则把队头出队
                if(hh<=tt&&k-q[hh]>s*v)hh++;
                //由队头的最大值更新f[k]
                if(hh<=tt)f[k]=max(f[k],g[q[hh]]+(k-q[hh])/v*w);
                //保持队列从大到小排序
                while(hh<=tt&&g[q[tt]]-(q[tt]-j)/v*w<g[k]-(k-j)/v*w)tt--;
                q[++tt]=k;
            }
        }
    }
    cout<<f[m];
    return 0;
}

分组背包问题

题目

N组物品和容量为V的背包,每组s个物品,每件物品体积v,价值w,只可以存1次,同组只能存一件物品,求不超过背包容量价值最大多少。

思路

代码

#include<bits/stdc++.h>
using namespace std;
const int mxn=1e2+5;
int n,m,s[mxn];
int v[mxn][mxn],w[mxn][mxn],dp[mxn];
int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>s[i];
        for(int j=1;j<=s[i];j++){
            cin>>v[i][j]>>w[i][j];
        }
    }
    for(int i=1;i<=n;i++){
        for(int j=m;j>0;j--){
            for(int k=1;k<=s[i];k++){
                if(j<v[i][k])continue;
                dp[j]=max(dp[j],dp[j-v[i][k]]+w[i][k]);
            }
        }
    }
    cout<<dp[m];
    return 0;
}

混合背包问题

题目

有 N 种物品和一个容量是 V 的背包。

物品一共有三类:

  • 第一类物品只能用1次(01背包);
  • 第二类物品可以用无限次(完全背包);
  • 第三类物品最多只能用 si 次(多重背包);

每种体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

思路

分解成多重背包问题

代码

#include<bits/stdc++.h>
using namespace std;
const int mxn=1e5+5;
int v[mxn],w[mxn],tot,dp[mxn];
int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    int n,V;
    cin>>n>>V;
    for(int i=1;i<=n;i++){
        int a,b,s;
        cin>>a>>b>>s;
        if(s==-1)s=1;
        else if(s==0)s=V/b;
        for(int j=1;j<=s;j<<=1){
            v[++tot]=j*a;
            w[tot]=j*b;
            s-=j;
        }
        if(s)v[++tot]=s*a,w[tot]=s*b;
    }
    for(int i=1;i<=tot;i++){
        for(int j=V;j>=v[i];j--){
            dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
        }
    }
    cout<<dp[V];
    return 0;
}

二维费用

题目

有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。

每件物品只能用一次。体积是 v i v_i vi,重量是 m i m_i mi,价值是 w i w_i wi

求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。
输出最大价值。

思路

代码

#include<bits/stdc++.h>
using namespace std;
const int mxn=105;
int dp[mxn][mxn];
int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    int n,V,M;
    cin>>n>>V>>M;
    for(int i=1;i<=n;i++){
        int v,m,w;
        cin>>v>>m>>w;
        for(int j=V;j>=v;j--){
            for(int k=M;k>=m;k--){
                dp[j][k]=max(dp[j][k],dp[j-v][k-m]+w);
            }
        }
    }
    cout<<dp[V][M];
    return 0;
}

潜水员

题目

潜水员为了潜水要使用特殊的装备。

他有一个带2种气体的气缸:一个为氧气,一个为氮气。

让潜水员下潜的深度需要各种数量的氧和氮。

潜水员有一定数量的气缸。

每个气缸都有重量和气体容量。

潜水员为了完成他的工作需要特定数量的氧和氮。

他完成工作所需气缸的总重的最低限度的是多少?

思路

为什么在 01 背包的时候, j<a[i] 和 k<b[i] 不能作为取值,因为我们背包的容量只有 j 和 k,放不下 a[i] 和 b[i]。但是在这里,假设我们把 a[i] 和 b[i] 装进背包,并不会有什么问题,因为 氧气和氮气 超出背包容量依然能满足。如果不装来反而会漏更新状态。

代码

#include<bits/stdc++.h>
using namespace std;
const int mxn=22,mxm=80,inf=0x3f3f3f3f;
int dp[mxn][mxm];
int main(){
    int M,V,n;
    memset(dp,inf,sizeof dp);
    cin>>M>>V>>n;
    dp[0][0]=0;
    for(int i=1;i<=n;i++){
        int u,v,w;
        cin>>u>>v>>w;
        for(int j=M;j>=0;j--){
            for(int k=V;k>=0;k--){
                if(j<=u&&k<=v){
                    dp[j][k]=min(dp[j][k],dp[0][0]+w);
                }
                else if(j<=u){
                    dp[j][k]=min(dp[j][k],dp[0][k-v]+w);
                }
                else if(k<=v){
                    dp[j][k]=min(dp[j][k],dp[j-u][0]+w);
                }
                else dp[j][k]=min(dp[j][k],dp[j-u][k-v]+w);
            }
        }
    }
    cout<<dp[M][V];
    return 0;
}

机器分配

题目

总公司拥有 M 台 相同 的高效设备,准备分给下属的 N 个分公司。

各分公司若获得这些设备,可以为国家提供一定的盈利。盈利与分配的设备数量有关。

问:如何分配这M台设备才能使国家得到的盈利最大?

求出最大盈利值。

分配原则:每个公司有权获得任意数目的设备,但总台数不超过设备数 M。

思路

转换成01背包问题,求以数量 j 为总数的结果,那么当公司 i 的取 k (k<=j) 个数量的最大盈利值 = 前面已数量 j - k 为总数的最大盈利值 + 该公司取 k 个设备的盈利值。

代码

#include<bits/stdc++.h>
using namespace std;
const int N=20;
int w[N][N],ans[N],dp[N][N];
int n,m;
void dfs(int p,int x){
    if(!p)return;
    for(int i=0;i<=x;i++){
        if(dp[p-1][x-i]+w[p][i]==dp[p][x]){
            ans[p]=i;
            dfs(--p,x-i);
            return;
        }
    }
}
int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            cin>>w[i][j];
        }
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            for(int k=0;k<=j;k++){
                dp[i][j]=max(dp[i][j],dp[i-1][j-k]+w[i][k]);
            }
        }
    }
    cout<<dp[n][m]<<'\n';
    dfs(n,m);
    for(int i=1;i<=n;i++)cout<<i<<' '<<ans[i]<<'\n';
    return 0;
}

有依赖的背包问题

题目

有 N 个物品和一个容量是 V 的背包。

物品之间具有依赖关系,且依赖关系组成一棵树的形状。如果选择一个物品,则必须选择它的父节点。

如下图所示:
QQ图片20181018170337.png

如果选择物品5,则必须选择物品1和2。这是因为2是5的父节点,1是2的父节点。

每件物品的编号是 i,体积是 vi,价值是 wi,依赖的父节点编号是 pi。物品的下标范围是 1…N1…。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。

输出最大价值。

思路

运用dfs算法。

因为要选一个点必须带上它的父节点,所以从根节点开始遍历。

因为可以选择多个子节点,所以每次更新应该更新父节点,所以应该从最底层开始向根节点遍历。

父节点体积 j 的最大价值 = 父节点体积 j - 体积 k 的最大值 + 子节点体积为 k 的最大价值。

代码

#include<bits/stdc++.h>
using namespace std;
const int N=110;
int v[N],w[N];
int dp[N][N];		//表示节点和体积
int n,m;
vector<int>g[N];

void dfs(int u){
    for(int i=m;i>=v[u];i--)dp[u][i]=w[u];
    for(int i=0;i<g[u].size();i++){
        int x=g[u][i];
        dfs(x);
        for(int j=m;j>=v[u];j--){
            for(int k=0;k<=j-v[u];k++){
                dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[x][k]);
            }
        }
    }
}

int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin>>n>>m;
    int x,root;
    for(int i=1;i<=n;i++){
        cin>>v[i]>>w[i]>>x;
        if(x==-1)root=i;
        else g[x].push_back(i);
    }
    dfs(root);
    cout<<dp[root][m];
    return 0;
}

背包问题求方案数

题目

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i 件物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出 最优选法的方案数。注意答案可能很大,请输出答案模 1e9+7 的结果。

思路

先初始化cnt赋1,因为背包什么都不装也是一种方案。

然后dp,如果新物品所能得到最大价值更大,改变当前体积的cnt。如果价值一样大,那么方案多了cnt[j-v]种。

代码

#include<bits/stdc++.h>
using namespace std;
const int N=1e3+5,mod=1e9+7;
int dp[N],cnt[N];
int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    int n,m;
    cin>>n>>m;
    for(int i=0;i<=m;i++)cnt[i]=1;
    for(int i=1;i<=n;i++){
        int v,w;
        cin>>v>>w;
        for(int j=m;j>=v;j--){
            if(dp[j]<dp[j-v]+w){
                cnt[j]=cnt[j-v];
                dp[j]=dp[j-v]+w;
            }
            else if(dp[j]==dp[j-v]+w){
                cnt[j]=(cnt[j]+cnt[j-v])%mod;
            }
        }
    }
    cout<<cnt[m];
    return 0;
}

状态机模型

包含多个待选状态,不同的状态之间有相互转化的方法,我们可以借助这些转化的手段,达成状态之间的相互转移

大盗阿福

题目

阿福是一名经验丰富的大盗。趁着月黑风高,阿福打算今晚洗劫一条街上的店铺。

这条街上一共有 N 家店铺,每家店中都有一些现金。

阿福事先调查得知,只有当他同时洗劫了两家相邻的店铺时,街上的报警系统才会启动,然后警察就会蜂拥而至。

作为一向谨慎作案的大盗,阿福不愿意冒着被警察追捕的风险行窃。

他想知道,在不惊动警察的情况下,他今晚最多可以得到多少现金?

思路

只要考虑抢不抢当前位置的店铺,可以得到当前最多可以获得的现金 = max ( 上个位置最多可以获得的现金,上上个位置最多可以获得的现金 + 当前店铺的现金 )。

代码

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int dp[N];
int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    int t,n;
    cin>>t;
    while(t--){
        cin>>n;
        for(int i=1;i<=n;i++){
            int x;
            cin>>x;
            dp[i]=max(dp[i-1],dp[i-2]+x);
        }
        cout<<dp[n]<<'\n';
    }
    return 0;
}

股票买卖 IV

题目

给定一个长度为 N 的数组,数组中的第 i 个数字表示一个给定股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润,你最多可以完成 k 笔交易。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。一次买入卖出合为一笔交易。

思路

初始化一开始所有的卖出操作都为0,买入操作都为inf,可以防止第 k 笔卖出前没有买入操作造成的影响。

因为买入、卖出操作是比较前一次买入操作和前一次卖出操作的最大利润,所以最后一次卖出操作的利润一定大于买入操作的利润。

代码

#include<bits/stdc++.h>
using namespace std;
const int K=205,inf=0xc0c0c0c0;
int dp[2][K][2];
int main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int n,k;
	cin>>n>>k;
	memset(dp,inf,sizeof dp);
	dp[0][0][1]=0;
	for(int i=1;i<=n;i++){
		int x;
		cin>>x;
		for(int j=0;j<=2*k;j+=2)dp[i&1][j][1]=0;
		for(int j=1;j<=k*2;j+=2){
			dp[i&1][j][0]=max(dp[(i-1)&1][j][0],dp[(i-1)&1][j-1][1]-x);
		}
		for(int j=2;j<=k*2;j+=2){
			dp[i&1][j][1]=max(dp[(i-1)&1][j][1],dp[(i-1)&1][j-1][0]+x);
		}
	}
	int ans=0;
	for(int i=1;i<=k*2;i++)ans=max(ans,dp[n&1][i][1]);
	cout<<ans;
	return 0;
}

股票买卖 V

题目

给定一个长度为 N 的数组,数组中的第 i 个数字表示一个给定股票在第 i 天的价格。

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

  • 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
  • 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

思路

第 i 天持有的利润 = max ( 第 i -1 天持有的利润 , 第 i - 2 天不持有的利润 )。

代码

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int dp[N][2];
int main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int n,x;
	cin>>n>>x;
	dp[1][0]=-x;
	for(int i=2;i<=n;i++){
		cin>>x;
		dp[i][0]=max(dp[i-1][0],dp[i-2][1]-x);
		dp[i][1]=max(dp[i-1][1],dp[i-1][0]+x);
	}
	cout<<dp[n][1];
	return 0;
}

状态压缩DP

所谓的状态压缩DP,就是用二进制数保存状态。为什么不直接用数组记录呢?因为用一个二进制数记录方便作位运算。前面做过的八皇后,八数码,也用到了状态压缩。

蒙德里安的梦想

题目

求把 N×M 的棋盘分割成若干个 1×2 的长方形,有多少种方案。

例如当 N=2,M=4, 时,共有 5 种方案。当 N=2,M=3 时,共有 3 种方案。

如下图所示:

2411_1.jpg

思路

代码

#include<bits/stdc++.h>
using namespace std;
const int mxn=12;
long long f[mxn][1<<mxn];
bool st[1<<mxn];
int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    int n,m;
    while(cin>>n>>m,n||m){
        for(int i=0;i<1<<n;i++){
            st[i]=true;
            int cnt=0;
            for(int j=0;j<n;j++){
                if(i>>j&1){
                    if(cnt&1){st[i]=false;break;}
                    cnt=0;
                }else cnt++;
            }
            if(cnt&1)st[i]=false;
        }
        memset(f,0,sizeof f);
        f[0][0]=1;
        for(int i=1;i<=m;i++){
            for(int j=0;j<1<<n;j++){
                for(int k=0;k<1<<n;k++){    
                    if((j&k)==0&&st[j|k])f[i][j]+=f[i-1][k];
                }
            }
        }
        cout<<f[m][0]<<'\n';
    }
    return 0;
}
对于if(st[k|j]&&(j&k)==0)更易懂的解释
//先判断第i-1列中的1个数是否合法
//再判断第i列中的1位置是否与i-1列冲突
	if(st[k|j]&&(j&k)==0)
	//st[k|j]解释:第i-1列中1的总个数
	//来源两部分:
	//st[k]表明第i-2列插入到i-1列的1的个数,i-1列被动生成的1
	//st[j]表明第i-1列插入到i列的1的个数,也就是i-1列自己生成的1
	f[i][j]+=f[i-1][k];

最短Hamilton路径

题目

给定一张 n 个点的带权无向图,点从 0∼n−1 标号,求起点 0 到终点 n−1 的最短 Hamilton 路径。

Hamilton 路径的定义是从 0 到 n−1 不重不漏地经过每个点恰好一次。

思路

用二进制表示走过的点,从1开始到(1<<n)-1,j 表示走到哪个点,k 表示走到 j 之前以 k 为终点的最短距离。

代码

#include<bits/stdc++.h>
using namespace std;
const int N=20,inf=0x3f3f3f3f;
int mp[N][N],dp[1<<N][N];
int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    int n;
    cin>>n;
    for(int i=0;i<n;i++){
        for(int j=0;j<n;j++){
            cin>>mp[i][j];
        }
    }
    memset(dp,inf,sizeof dp);
    dp[0][0]=0;
    for(int i=0;i<1<<n;i++){
        for(int j=0;j<n;j++){
            if((i>>j)&1){
                for(int k=0;k<n;k++){
                    if((i>>k)&1){
                        dp[i][j]=min(dp[i][j],dp[i-(1<<j)][k]+mp[j][k]);
                    }
                }
            }
        }
    }
    cout<<dp[(1<<n)-1][n-1];
    return 0;
}

玉米田

题目

农夫约翰的土地由 M×N 个小方格组成,现在他要在土地里种植玉米。

非常遗憾,部分土地是不育的,无法种植。

而且,相邻的土地不能同时种植玉米,也就是说种植玉米的所有方格之间都不会有公共边缘。

现在给定土地的大小,请你求出共有多少种种植方法。

土地上什么都不种也算一种方法。

思路

先把土地的情况用二进制表示,因为每一行最多只有12列,所以可以用二进制来表示所有状态,然后筛选出满足条件的状态用v存储。满足条件的状态最多只有144个,所以双重for循环将可以相邻的状态用g来存储。给第一行满足土地条件的状态赋1,向下一行遍历,直到遍历完全,将最后一行的各个状态出现的次数求和即为答案。

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N=13,M=1<<N,mod=1e8;
ll mp[N];
vector<ll>v,g[M];
ll dp[N][M];
bool check(ll x){
    return x&(x>>1);
}
int main(){
    ll n,m;
    cin>>n>>m;
    for(ll i=0;i<n;i++){
        for(ll j=m-1;j>=0;j--){
            ll x;
            cin>>x;
            if(!x)continue;
            mp[i]+=pow(2,j);
        }
    }
    for(ll i=0;i<1<<m;i++){		//保存所有可行的状态,v的最大总数是144
        if(check(i))continue;
        v.push_back(i);
    }
    for(ll i=0;i<v.size();i++){
        for(ll j=0;j<v.size();j++){
            if(v[i]&v[j])continue;
            g[v[i]].push_back(v[j]);		//保存所有上下不相连状态
        }
    }
    for(ll i=0;i<v.size();i++){
        if((v[i]|mp[0])^mp[0])continue;
        dp[0][v[i]]=1;		//筛出所有满足条件的状态
    }
    for(ll i=1;i<n;i++){
        for(ll j=0;j<v.size();j++){
            for(ll t=0;t<g[v[j]].size();t++){
                ll y=g[v[j]][t];
                if((y|mp[i])^mp[i])continue;
                dp[i][y]+=dp[i-1][v[j]];
            }
        }
    }
    ll ans=0;
    for(ll i=0;i<v.size();i++)ans+=dp[n-1][v[i]];
    cout<<ans%mod;
    return 0;
}

炮兵阵地

题目

司令部的将军们打算在 N×M 的网格地图上部署他们的炮兵部队。

一个 N×M 的地图由 N 行 M 列组成,地图的每一格可能是山地(用 H 表示),也可能是平原(用 P 表示),如下图。

在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队);一支炮兵部队在地图上的攻击范围如图中黑色区域所示:

1185_1.jpg

如果在地图中的灰色所标识的平原上部署一支炮兵部队,则图中的黑色的网格表示它能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。

图上其它白色网格均攻击不到。

从图上可见炮兵的攻击范围不受地形的影响。

现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内最多能够摆放多少我军的炮兵部队。

思路

先把所有状态列出来,再将可以相邻的状态列出来,因为要满足条件左右相隔至少2个单位,所以满足条件的状态最多只有60个,最后向下遍历每行,如果当前状态满足这一行所处的地形条件,并且满足这一行与上一行不误伤,并与上一行满足条件的上一行也不误伤,则这一行最多可以摆放的炮兵数量=可以相邻的上一行中最大满足的炮兵数量+这个状态的炮兵数量。

代码

#include<bits/stdc++.h>
using namespace std;
const int N=110,M=11;
int mp[N];
vector<int>v;
vector<int>g[1<<M];
int dp[2][1<<M][1<<M];
int nb[1<<M];
bool check(int x){
    return (x&(x>>1)||x&(x>>2));
}
int count(int x){
    int r=0;
    while(x){
        r+=(x&1);
        x>>=1;
    }
    return r;
}
int main(){
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        string s;
        cin>>s;
        for(int j=0;j<m;j++){
            mp[i]+=(s[j]=='P')<<(m-1-j);
        }
    }
    for(int i=0;i<1<<m;i++){	//满足条件的v的最大总数是60,所以四层循环不会超时
        if(check(i))continue;
        v.push_back(i);
        nb[i]=count(i);
    }
    for(int i=0;i<v.size();i++){
        for(int j=0;j<v.size();j++){
            if(v[i]&v[j])continue;
            g[v[i]].push_back(v[j]);
        }
    }
    int ans=0;
    for(int i=1;i<=n;i++){    
        for(int j=0;j<v.size();j++){            //当前状态
            int a=v[j];
            if((a|mp[i])^mp[i])continue;
            for(int k=0;k<g[a].size();k++){     //上一状态
                int b=g[a][k];
                for(int u=0;u<g[b].size();u++){    //上上状态
                    int c=g[b][u];
                    if(c&a)continue;
                    dp[i&1][a][b]=max(dp[i&1][a][b],dp[(i-1)&1][b][c]+nb[a]);
                    ans=max(ans,dp[i&1][a][b]);
                }
            }
        }
    }
    cout<<ans;
    return 0;
}

区间DP

石子合并

题目

设有 N 堆石子排成一排,其编号为 1,2,3,…,N。

每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N 堆石子合并成为一堆。

每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。

问题是:找出一种合理的方法,使总的代价最小,输出最小代价。

思路

代码

#include<bits/stdc++.h>
using namespace std;
const int mxn=305;
int a[mxn],dp[mxn][mxn];
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i],a[i]+=a[i-1];
    memset(dp,0x3f3f3f3f,sizeof dp);
    for(int len=1;len<=n;len++){	//区间长度从1到n合并
        for(int i=1;i+len-1<=n;i++){
            int j=i+len-1;
            if(i==j){
                dp[i][j]=0;
                continue;
            }
            for(int k=i;k<j;k++){
                dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+a[j]-a[i-1]);
            }
        }
    }
    cout<<dp[1][n];
    return 0;
}

合并果子

题目

在一个果园里,达达已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。

达达决定把所有的果子合成一堆。

每一次合并,达达可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。

可以看出,所有的果子经过 n−1 次合并之后,就只剩下一堆了。

思路

代码

#include<bits/stdc++.h>
using namespace std;
priority_queue<int,vector<int>,greater<int>>q;
int main(){
    int n,x;
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>x;
        q.push(x);
    }
    int ans=0;
    while(q.size()>1){		//贪心,每次找重量最小的两个堆
        int a=q.top();q.pop();
        int b=q.top();q.pop();
        ans+=a+b;
        q.push(a+b);
    }
    cout<<ans;
    return 0;
}

树形DP

树形DP整体的思路就是用树形的结构存储数据。

树形DP的关键和实现方法是dfs。

先找到树根,从树根开始运用dfs递归,跟dfs一样先初始化,然后递归到叶子节点上为止,把最底层的 dp [ i ] [ j ] 更新完毕,再回来往上走,自底向上地根据题意更新上层的 dp 数组,最后输出答案即可。

一般基础的题转移方程有两种模式:
选择节点类

  • dp [ i ] [ 0 ] = dp [ j ] [ 1 ]

  • dp [ i ] [ 1 ] = max ⁡ / min ⁡ ( f [ j ] [ 0 ] , f [ j ] [ 1 ] )

选择节点式的题首先前提条件是整个数据是由树形结构存储的,或者应该用树形结构存,效率更高什么的,然后会告诉你相邻的节点是不能同时存在的,要求取最大最小值

树形背包类

  • dp [ v ] [ k ] = dp [ u ] [ k ] + val

  • dp [ u ] [ k ] = m a x ( dp [ u ] [ k ] , dp [ v ] [ k − 1 ] )

树形背包,就是背包加上条件,一个物品只有选择了它的主件(根节点)才能选择

没有上司的舞会

题目

Ural 大学有 N 名职员,编号为 1∼N。

他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。

每个职员有一个快乐指数,用整数 Hi 给出,其中 1≤i≤N。

现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。

在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。

思路

因为是有向边,不用考虑会回溯,所以dfs只要一个变量,选择节点类,dp[0]表示不取该点,dp[1]表示取点,则父节点的dp[0]=所有它的子节点的最大快乐指数即max(dp[0],dp[1]),然后这个父节点的dp[1]=所有它的子节点的dp[0]的值的总和,最后答案应该是从取根节点所得的值和不取根节点所得的值中取最大。

代码

#include<bits/stdc++.h>
using namespace std;
const int N=6e3+5;
int to[N<<1],from[N<<1],head[N],idx;
int w[N],b[N],dp[N][2];
void add(int u,int v){
    to[idx]=v,from[idx]=head[u],head[u]=idx++;
}
void dfs(int u){
    dp[u][1]=w[u];
    for(int i=head[u];i!=-1;i=from[i]){
        int j=to[i];
        dfs(j);
        dp[u][0]+=max(dp[j][0],dp[j][1]);
        dp[u][1]+=dp[j][0];
    }
}
int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    int n;
    cin>>n;
    memset(head,-1,sizeof head);
    for(int i=1;i<=n;i++)cin>>w[i];
    for(int i=1;i<n;i++){
        int u,v;
        cin>>v>>u;
        add(u,v);
        b[v]++;
    }
    int root=1;
    while(b[root])root++;
    dfs(root);
    cout<<max(dp[root][0],dp[root][1]);
    return 0;
}

树的中心

题目

给定一棵树,树中包含 n 个结点(编号1~n)和 n−1 条无向边,每条边都有一个权值。

请你在树中找到一个点,使得该点到树中其他结点的最远距离最近。

树tree

思路

找每个点到其它节点的最远距离,先设1为根节点,向下遍历,找最远和次远的两条路径,并且记录各路径的根节点。然后向上遍历,如果父节点的最远向下路径的根节点是子节点,则子节点的最远向上距离=max(该节点的最远向下距离,父节点的次远向下距离)+这个点到父节点的距离。否则子节点的最远向上距离=max(该节点的最远向下距离,父节点的最远向下距离)+这个点到父节点的距离。这样就能获得每个点向上和向下的最远距离。

因为向下遍历的时候,要先知道子节点最远向下距离,所以要先遍历子节点。而向上遍历的时候,要先知道父节点的最远向上距离,所以先遍历父节点。

代码

#include<bits/stdc++.h>
using namespace std;
const int N=1e4+5;
int to[N<<1],val[N<<1],from[N<<1],head[N],idx;
int d1[N],d2[N],s1[N],s2[N],up[N];

void add(int u,int v,int w){
    to[idx]=v,val[idx]=w,from[idx]=head[u],head[u]=idx++;
}

void dfs_d(int u,int father){		//向下遍历,找最长和次长的路线
    for(int i=head[u];i!=-1;i=from[i]){
        int j=to[i];
        if(j==father)continue;
        dfs_d(j,u);
        if(d1[j]+val[i]>=d1[u]){
            d2[u]=d1[u],s2[u]=s1[u];
            d1[u]=d1[j]+val[i],s1[u]=j;
        }
        else if(d1[j]+val[i]>d2[u]){
            d2[u]=d1[j]+val[i],s2[u]=j;
        }
    }
}

void dfs_u(int u,int father){
    for(int i=head[u];i!=-1;i=from[i]){
        int j=to[i];
        if(j==father)continue;
        if(s1[u]==j)up[j]=val[i]+max(up[u],d2[u]);
        else up[j]=val[i]+max(up[u],d1[u]);
        dfs_u(j,u);
    }
}

int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    int n;
    cin>>n;
    memset(head,-1,sizeof head);
    for(int i=1;i<n;i++){
        int u,v,w;
        cin>>u>>v>>w;
        add(u,v,w);
        add(v,u,w);
    }
    dfs_d(1,-1);		//设1为根节点
    dfs_u(1,-1);
    int ans=0x3f3f3f3f;
    for(int i=1;i<=n;i++)ans=min(ans,max(d1[i],up[i]));
    cout<<ans;
    return 0;
}

数字转换

题目

如果一个数 x 的约数之和 y(不包括他本身)比他本身小,那么 x 可以变成 y,y 也可以变成 x。

例如,4 可以变为 3,1 可以变为 7。

限定所有数字变换在不超过 n 的正整数范围内进行,求不断进行数字变换且不出现重复数字的最多变换步数。

思路

先求出每个数的约数和,然后遍历一遍所有的数,如果约数和小于这个数,则给这两个值连接起来,然后就是建树,找出一个点到其它点的最远和次远的距离。

代码

#include<bits/stdc++.h>
using namespace std;
const int N=5e4+5;
int mp[N],d1[N],d2[N];
int to[N<<1],from[N<<1],head[N],idx;
void add(int u,int v){
    to[idx]=v,from[idx]=head[u],head[u]=idx++;
}

void dfs(int u,int father){
    for(int i=head[u];i!=-1;i=from[i]){
        int j=to[i];
        if(j==father)continue;
        dfs(j,u);
        if(d1[u]<=d1[j]+1){
            d2[u]=d1[u];
            d1[u]=d1[j]+1;
        }
        else if(d2[u]<d1[j]+1){
            d2[u]=d1[j]+1;
        }
    }
}

int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        for(int j=2;j<=n;j++){
            if(i*j>n)break;
            mp[i*j]+=i;
        }
    }
    memset(head,-1,sizeof head);
    mp[1]=1;
    for(int i=1;i<=n;i++){
        if(i<=mp[i])continue;
        add(i,mp[i]);
        add(mp[i],i);
    }
    dfs(1,-1);		//为什么可以是1?
    cout<<d1[1]+d2[1];
    return 0;
}

二叉苹果树

题目

有一棵二叉苹果树,如果树枝有分叉,一定是分两叉,即没有只有一个儿子的节点。

这棵树共 N 个节点,编号为 1 至 N,树根编号一定为 1。

我们用一根树枝两端连接的节点编号描述一根树枝的位置。

一棵苹果树的树枝太多了,需要剪枝。但是一些树枝上长有苹果,给定需要保留的树枝数量,求最多能留住多少苹果。

这里的保留是指最终与1号点连通。

思路

依赖背包DP,dp表示以i为根节点的子树,包含i的连通块边数为j的方案。从树的最底层开始遍历,将最底层连通块的方案求出来以后,往上遍历,父节点的边数(j)=父节点的边数(j-k-1)+子节点的边数(k)+父节点到子节点的值。

代码

#include<bits/stdc++.h>
using namespace std;
const int N=110,M=N<<1;
int to[M],val[M],from[M],head[N],idx;
int dp[N][N],n,m;

void add(int u,int v,int w){
    to[idx]=v,val[idx]=w,from[idx]=head[u],head[u]=idx++;
}

void dfs(int u,int father){
    for(int i=head[u];i!=-1;i=from[i]){
        int t=to[i];		//t表示子树
        if(t==father)continue;
        dfs(t,u);
        for(int j=m;j>=0;j--){		//防止重复取一条边
            for(int k=0;k<j;k++){
                dp[u][j]=max(dp[u][j],dp[u][j-k-1]+dp[t][k]+val[i]);
            }
        }
    }
}

int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin>>n>>m;
    memset(head,-1,sizeof head);
    for(int i=1;i<n;i++){
        int u,v,w;
        cin>>u>>v>>w;
        add(u,v,w);
        add(v,u,w);
    }
    dfs(1,-1);
    cout<<dp[1][m];
    return 0;
}

计数类DP

整数划分

题目

一个正整数 n 可以表示成若干个正整数之和,形如:n=n1+n2+…+nk,其中 n1≥n2≥…≥nk,k≥1。

我们将这样的一种表示称为正整数 n 的一种划分。

现在给定一个正整数 n,请你求出 n 共有多少种不同的划分方法。

思路

代码

#include<bits/stdc++.h>
using namespace std;

int get(vector<int>num,int l,int r){
    int res=0;
    for(int i=l;i>=r;i--){
        res=res*10+num[i];
    }
    return res;
}

int power10(int x){
    int res=1;
    while(x--)res*=10;
    return res;
}

int count(int n,int x){
    if(!n)return 0;
    vector<int>num;
    while(n){
        num.push_back(n%10);
        n/=10;
    }
    n=num.size();
    int res=0;
    for(int i=n-1-!x;i>=0;i--){     //!x不能第一位是0
        if(i<n-1){
            res+=get(num,n-1,i+1)*power10(i);
            if(!x)res-=power10(i);      //如果要枚举0,得从001~abc-1
        }
        if(num[i]==x)res+=get(num,i-1,0)+1;
        else if(num[i]>x)res+=power10(i);
    }
    return res;
}

int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    int a,b;
    while(cin>>a>>b,a||b){
        if(a>b)swap(a,b);
        for(int i=0;i<10;i++)
            cout<<count(b,i)-count(a-1,i)<<' ';
        cout<<'\n';
    }
    return 0;
}

记忆化搜索

滑雪

题目

给定一个 R 行 C 列的矩阵,表示一个矩形网格滑雪场。矩阵中第 i 行第 j 列的点表示滑雪场的第 i 行第 j 列区域的高度。一个人从滑雪场中的某个区域内出发,每次可以向上下左右任意一个方向滑动一个单位距离。当然,一个人能够滑动到某相邻区域的前提是该区域的高度低于自己目前所在区域的高度。

现在给定你一个二维矩阵表示滑雪场各区域的高度,请你找出在该滑雪场中能够完成的最长滑雪轨迹,并输出其长度(可经过最大区域数)。

思路

代码

#include<bits/stdc++.h>
using namespace std;
const int mxn=305;
int mp[mxn][mxn],f[mxn][mxn],ans,n,m;
int gx[4]={1,0,-1,0},gy[4]={0,1,0,-1};
int dfs(int x,int y){
    int &v=f[x][y];		//引用
    if(v!=-1)return v;		//找过的点不找
    v=1;
    for(int i=0;i<4;i++){
        int xx=x+gx[i];
        int yy=y+gy[i];
        if(xx<1||xx>n)continue;
        if(yy<1||yy>m)continue;
        if(mp[xx][yy]>=mp[x][y])continue;
        v=max(v,dfs(xx,yy)+1);		//找四周比该点低的位置的
    }
    return v;
}
int main(){
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            cin>>mp[i][j];
    memset(f,-1,sizeof f);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            ans=max(ans,dfs(i,j));
    cout<<ans;
    return 0;
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值