DP 进阶 呼啦啦

数字三角形模型

1015. 摘花生 - AcWing题库 (单条路线)

简单

d p [ i ] [ j ] dp[i][j] dp[i][j]表示从(1,1)走到(i,j)的最大花生数量

d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) + b [ i ] [ j ] dp[i][j] = max(dp[i-1][j],dp[i][j-1]) + b[i][j] dp[i][j]=max(dp[i1][j],dp[i][j1])+b[i][j]

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int MAXN = 105;
int T, R, C;
int dp[MAXN][MAXN], b[MAXN][MAXN];

int main()
{
    scanf("%d",&T);
    while(T--)
    {
        memset(dp,0,sizeof(dp));
        scanf("%d%d",&R,&C);
        for(int i = 1;i <= R;i++)
        {
            for(int j = 1;j <= C;j++)
            {
                scanf("%d",&b[i][j]);
            }
        }
        for(int i = 1;i <= R;i++)
        {
            for(int j = 1;j <= C;j++)
            {
                dp[i][j] = max(dp[i-1][j],dp[i][j-1]) + b[i][j];
            }
        }
        printf("%d\n",dp[R][C]);
    }
    return 0;
}

1018. 最低通行费 - AcWing题库 (单条路线+边界判断)

只能往下或往右走,注意边界。

第一排只能从左边走过来

第一列只能从上面走下来

#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;
const int MAXN = 105;
int b[MAXN][MAXN], dp[MAXN][MAXN], n;

int main()
{
    memset(dp,0,sizeof(dp));
    scanf("%d",&n);
    for(int i = n;i >= 1;i--)
    {
        for(int j = 1;j <= n;j++)
        {
            scanf("%d",&b[i][j]);
        }
    }
    for(int i = n;i >= 1;i--)
    {
        for(int j = 1;j <= n;j++)
        {
            if(i == n) dp[i][j] = dp[i][j-1] + b[i][j];
            else if(j == 1) dp[i][j] = dp[i+1][j] + b[i][j];
            else dp[i][j] = min(dp[i+1][j],dp[i][j-1]) + b[i][j];
        }
    }
    printf("%d\n",dp[1][n]);
    return 0;
}

1027. 方格取数 - AcWing题库 (两条路线)

题意:从(1,1)走到终点的两条路径最大值,两条路径不能重复

f [ i 1 , j 1 , i 2 , j 2 ] f[i1,j1,i2,j2] f[i1,j1,i2,j2]​表示从(1,1)到(i1,j1)与(1,1)到(i2,j2)的路径最大值

如何处理走过的格子不重复

只有在 i 1 + j 1 = i 2 + j 2 i_1+j_1 = i_2+j_2 i1+j1=i2+j2的时候走过的方格才能重合

进行优化

f [ k , i 1 , i 2 ] f[k,i1,i2] f[k,i1,i2]表示从(1,1)到(i1, k-i1)与(1,1)到(i2,k-i2)的路径最大值

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int MAXN = 11;
int n;
int w[MAXN][MAXN], dp[2*MAXN][MAXN][MAXN];

int main()
{
    scanf("%d",&n);
    while(true)
    {
        int a, b, c;
        scanf("%d%d%d",&a,&b,&c);
        if(!a && !b && !c) break;
        w[a][b] = c;
        
        for(int k = 2;k <= n+n;k++)
        {
            for(int i1 = 1;i1 <= n;i1++)
            {
                for(int i2 = 1;i2 <= n;i2++)
                {
                    int j1 = k-i1, j2 = k-i2;
                    if(j1 >= 1 && j1 <= n && j2 >= 1 && j2 <= n)
                    {
                        int add = w[i1][j1];
                        if(i1 != i2) add += w[i2][j2];
                        int &x = dp[k][i1][i2];
                        x = max(x, dp[k-1][i1-1][i2-1] + add);
                        x = max(x, dp[k-1][i1][i2-1] + add);
                        x = max(x, dp[k-1][i1-1][i2] + add);
                        x = max(x, dp[k-1][i1][i2] + add);
                    }
                }
            }
        }
    }
    int ans = dp[n+n][n][n];
    printf("%d\n",ans);
    return 0;
}

275. 传纸条 - AcWing题库 (两条路线+思维)

求(1,1)到(m,n)的最大两条路线,两条路线不能经过同一点

与上题一模一样,证明如下

如线有交叉:可如下处理

image-20210724210524185

处理后仍可能通过相同的点

image-20210724210612909

第二次通过C权值为0,A和B的权值>=0,两次通过同一点的路线并不会使总和更大。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
int n, m;
int w[55][55], dp[101][55][55];

int main()
{
    scanf("%d%d",&m,&n);
    for(int i = 1;i <= m;i++)
    {
        for(int j = 1;j <= n;j++)
        {
            scanf("%d",&w[i][j]);
        }
    }
    for(int k = 2;k <= n+m;k++)
    {
        for(int i1 = 1;i1 <= m;i1++)
        {
            for(int i2 = 1;i2 <= m;i2++)
            {
                int j1 = k-i1, j2 = k-i2;
                if(j1 >= 1 && j1 <= n && j2 >= 1 && j2 <= n)
                {
                    int t = w[i1][j1];
                    if(i1 != i2) t += w[i2][j2];
                    int &x = dp[k][i1][i2];
                    x = max(x, dp[k-1][i1-1][i2-1] + t);
                    x = max(x, dp[k-1][i1][i2-1] + t);
                    x = max(x, dp[k-1][i1-1][i2] + t);
                    x = max(x, dp[k-1][i1][i2] + t);
                } 
            }
        }
    }
    int ans = dp[n+m][m][m];
    printf("%d\n",ans);
    return 0;
}

最长上升子序列模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AAkCY4sw-1629966810410)(C:/Users/DELL/AppData/Roaming/Typora/typora-user-images/image-20210725104643636.png)]

1017. 怪盗基德的滑翔翼 - AcWing题库 (模板)

题意:从序列中某一点开始向两端的最长下降子序列。

从头开始和从尾开始跑一遍最长上升子序列。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int MAXN = 101;
int T, n, a[MAXN], b[MAXN], c[MAXN], id;

int main()
{
    scanf("%d",&T);
    while(T--)
    {
        int ans = 1;
        memset(a,0,sizeof(a));
        memset(b,0,sizeof(b));
        memset(c,0,sizeof(c));
        scanf("%d",&n);
        id = 0;
        for(int i = 1;i <= n;i++) scanf("%d",&a[i]);
        b[id++] = a[1];
        for(int i = 2;i <= n;i++)
        {
            if(a[i] <= b[id-1])
            {
                int pos = lower_bound(b,b+id,a[i])-b;
                b[pos] = a[i];
            }
            else b[id++] = a[i];
        }
        ans = max(ans,id);
        id = 0;
        c[id++] = a[n];
        for(int i = n-1;i >= 1;i--)
        {
            if(a[i] <= c[id-1])
            {
                int pos = lower_bound(c,c+id,a[i])-c;
                c[pos] = a[i];
            }
            else c[id++] = a[i];
        }
        ans = max(ans,id);
        printf("%d\n",ans);
    }
    return 0;
}

1014. 登山 - AcWing题库 (先上升后下降,枚举top)

题意:登山,先上升后下降,不能连续游览相同高度的两个景点

枚举峰值的点。前后各跑一个最长上升子序列,后一段满足下降时的最大值不超过top O ( n 2 l o g n ) O(n^2logn) O(n2logn)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int MAXN = 1001;
int n, a[MAXN], b[MAXN], c[MAXN];

int main()
{
    scanf("%d",&n);
    for(int i = 1;i <= n;i++)   scanf("%d",&a[i]);
    int ans = 0;
    for(int i = 1;i <= n;i++)
    {
        int top = i;
        int id = 0, id2 = 0;
        b[id++] = a[1];
        for(int j = 2;j <= top;j++)
        {
            if(a[j] <= b[id-1]) 
            {
                int pos = lower_bound(b,b+id,a[j]) - b;
                b[pos] = a[j];
            }
            else b[id++] = a[j];
        }
        for(int j = n;j >= top+1;j--)
        {
            if(a[j] < b[id-1])
            {
                if(a[j] <= c[id2-1])
                {
                    int pos = lower_bound(c,c+id2,a[j]) - c;
                    c[pos] = a[j];
                }
                else c[id2++] = a[j];    
            }
        }
        ans = max(ans,id+id2);
    }
    printf("%d\n",ans);
    return 0;
}

可以先打表判断每个点开始的最长上升子序列和最长下降子序列,最后进行相加。复杂度 O ( n log ⁡ n ) O(n \log n) O(nlogn)

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>

using namespace std;
const int M = 1001;
int n, a[M], f[M], g[M], b[M], c[M], id;

int main()
{
    scanf("%d",&n);
    int ans = 0;
    for(int i = 1;i <= n;i++) scanf("%d",&a[i]);
    b[id++] = a[1], f[1] = 1;
    for(int i = 2;i <= n;i++)
    {
        if(a[i] <= b[id-1])
        {
            int pos = lower_bound(b,b+id,a[i]) - b;
            b[pos] = a[i];
            f[i] = id;
        }
        else {
            b[id++] = a[i];
            f[i] = id;
        }
    }
    
    int id = 0;
    g[n] = 1, c[id++] = a[n];
    for(int i = n-1;i >= 1;i--)
    {
        if(a[i] <= c[id-1])
        {
            int pos = lower_bound(c,c+id,a[i]) - c;
            c[pos] = a[i];
            g[i] = id;
        }
        else {
            c[id++] = a[i];
            g[i] = id;
        }
    }
    
    for(int i = 1;i <= n;i++)
    {
        ans = max(ans,f[i]+g[i]-1);
    }
    printf("%d\n",ans);
    
    return 0;
}

482. 合唱队形 - AcWing题库

思路与上题一样

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int MAXN = 101;
int n, a[MAXN], b[MAXN], f[MAXN], c[MAXN], g[MAXN], id;


int main()
{
    scanf("%d",&n);
    for(int i = 1;i <= n;i++) scanf("%d",&a[i]);
    b[id++] = a[1], f[1] = 1;
    for(int i = 2;i <= n;i++)
    {
        if(a[i] <= b[id-1])
        {
            int pos = lower_bound(b,b+id,a[i]) - b;
            b[pos] = a[i];
            f[i] = id;
        }
        else
        {
            b[id++] = a[i];
            f[i] = id;
        }
    }
    id = 0;
    c[id++] = a[n], g[n] = 1;
    for(int i = n-1;i >= 1;i--)
    {
        if(a[i] <= c[id-1])
        {
            int pos = lower_bound(c,c+id,a[i]) - c;
            c[pos] = a[i];
            g[i] = id;
        }
        else
        {
            c[id++] = a[i];
            g[i] = id;
        }
    }
    int ans = 1e9;
    for(int i = 1;i <= n;i++)
    {
        ans = min(ans,n-f[i]-g[i]+1);
    }
    printf("%d\n",ans);
    return 0;
}

1012. 友好城市 - AcWing题库 (排序+模板)

按照左端点排序,右端点最长上升子序列

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int MAXN = 5005;
int n, b[MAXN], id;
struct node{
    int l, r;
    bool operator<(const node &a)
    {
        return l < a.l;
    }
}N[MAXN];

int main()
{
    scanf("%d",&n);
    for(int i = 1;i <= n;i++)
    {
        int l, r;
        scanf("%d%d",&l,&r);
        N[i].l = l;
        N[i].r = r;
    }
    sort(N+1,N+1+n);
    //for(int i = 1;i <= n;i++) printf("%d %d\n",N[i].l,N[i].r);
    b[id++] = N[1].r;
    for(int i = 2;i <= n;i++)
    {
        if(N[i].r < b[id-1])
        {
            int pos = lower_bound(b,b+id,N[i].r) - b;
            b[pos] = N[i].r;
        }
        else b[id++] = N[i].r;
    }
    printf("%d\n",id);
    return 0;
}

1016. 最大上升子序列和 - AcWing题库 (sum)

题意:求一个序列上升子序列之和最大值

s u m [ i ] sum[i] sum[i]表示以 a [ i ] a[i] a[i]结尾的上升子序列之和最大值

s u m [ i ] = m a x ( s u m [ j ] + a [ i ] ) ( a [ i ] > a [ j ] ) sum[i] = max(sum[j]+a[i]) \quad (a[i] > a[j]) sum[i]=max(sum[j]+a[i])(a[i]>a[j])

初始化 s u m [ i ] = a [ i ] sum[i] = a[i] sum[i]=a[i]

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int MAXN = 1010;
int n, dp[MAXN], sum[MAXN], a[MAXN];

int main()
{
    scanf("%d",&n);
    for(int i = 1;i <= n;i++) scanf("%d",&a[i]);
    dp[1] = 1, sum[1] = a[1];
    for(int i = 2;i <= n;i++)
    {
        sum[i] = a[i], dp[i] = 1;
        for(int j = 1;j < i;j++)
        {
            if(a[i] > a[j])
            {
                sum[i] = max(sum[j] + a[i], sum[i]);
                dp[i] = max(dp[i], dp[j] + 1);
            }
        }
    }
    int ans = sum[1];
    for(int i = 1;i <= n;i++) ans = max(ans,sum[i]);
    printf("%d\n",ans);
    return 0;
}

1010. 拦截导弹 - AcWing题库 (贪心+模板)

题意:对于每个导弹拦截系统,它只能拦截比前一个导弹高度小的导弹,问最少要多少个拦截系统。

从头到尾与从尾到头最长上升子序列

贪心的思想:对于每个导弹,要么开辟一个新的系统,要么把他放到系统中第一个比他大的末尾,这样保证每个系统末尾的高度为单调上升的。

此时,需要的系统数为序列最长上升子序列。

注意lower_boundupper_bound

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int MAXN = 1010;
int n, a[MAXN], dp1[MAXN], dp2[MAXN], id;

int main()
{
    while(cin >> a[++id]);
    int cnt1 = 0, cnt2 = 0;
    dp1[cnt1++] = a[1], dp2[cnt2++] = a[id-1];
    for(int i = 2;i <= id-1;i++)
    {
        if(a[i] <= dp1[cnt1-1])
        {
            int pos = lower_bound(dp1,dp1+cnt1,a[i]) - dp1;
            dp1[pos] = a[i];
        }
        else dp1[cnt1++] = a[i];
    }
    
    for(int i = id-2;i >= 1;i--)
    {
        if(a[i] < dp2[cnt2-1])
        {
            int pos = upper_bound(dp2,dp2+cnt2,a[i]) - dp2;
            dp2[pos] = a[i];
        }
        else dp2[cnt2++] = a[i];
    }
    
    printf("%d\n%d\n",cnt2,cnt1);
    
    return 0;
}

🔺187. 导弹防御系统 - AcWing题库(贪心+dp+dfs)

题意:对于每个导弹拦截系统,他拦截的导弹只能单调上升或者单调下降,问最小需要的防御系统的数量。

一次枚举每个数:

先枚举该数在单调上升的子序列中,还是单调下降的字序列中

如果该数在单调上升的序列中,则枚举在哪个单调上升的序列后面。

如果该数被放到了单调下降的序列中,则枚举在那个单调下降的序列后面。

优化:

u p [ ] up[] up[]存储所有上升序列的末尾元素

d o w n [ ] down[] down[]​存储所有下降序列的末尾元素

贪心的放到上升序列的恰好比他小的元素后面

同理,贪心的放到下降序列恰好比他大的元素后面

采用迭代加深的方式

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int MAXN = 60;
int n;
int h[MAXN], up[MAXN], down[MAXN];

bool dfs(int depth, int u, int su, int sd)
{
    if(su + sd > depth) return false;
    if(u > n) return true;
    bool flag = false;
    // up
    for(int i = 1;i <= su;i++)
    {
        if(h[u] > up[i])
        {
            int t = up[i];
            up[i] = h[u];
            if(dfs(depth, u+1, su, sd)) return true;
            up[i] = t;
            flag = true;
            break;
        }
    }
    if(!flag) 
    {
        up[su+1] = h[u];
        if(dfs(depth, u+1, su+1, sd)) return true;
    }
    
    flag = false;
    // down
    for(int i = 1;i <= sd;i++)
    {
        if(h[u] < down[i])
        {
            int t = down[i];
            down[i] = h[u];
            if(dfs(depth, u+1, su, sd)) return true;
            down[i] = t;
            flag = true;
            break;
        }
    }
    if(!flag)
    {
        down[sd+1] = h[u];
        if(dfs(depth, u+1, su, sd+1)) return true;
    }
    return false;
}

int main()
{
    while(true)
    {
        scanf("%d",&n);
        if(!n) break;
        for(int i = 1;i <= n;i++) scanf("%d", &h[i]);
        int depth = 0;
        while(!dfs(depth, 1, 0, 0)) depth++;
        printf("%d\n",depth);
    }
    return 0;
}

🔺272. 最长公共上升子序列 - AcWing题库

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xs6J5i9C-1629966810412)(C:/Users/DELL/AppData/Roaming/Typora/typora-user-images/image-20210728131817651.png)]

d p [ i ] [ j ] dp[i][j] dp[i][j]表示 a [ 1 − i ] , b [ 1 − j ] a[1-i],b[1-j] a[1i],b[1j],且以 b [ j ] b[j] b[j]结尾的最长公共上升子序列长度

当最长公共上升子序列不包含 a [ i ] a[i] a[i] d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i-1][j] dp[i][j]=dp[i1][j]

当最长公共上升子序列包含 a [ i ] a[i] a[i]​ 等价于 a [ i ] = b [ j ] a[i] = b[j] a[i]=b[j]

最长上升子序列以 b [ j ] b[j] b[j]结尾 d p [ i ] [ j ] = 1 dp[i][j] = 1 dp[i][j]=1

倒数第二个元素是 b [ 1 ] b[1] b[1]子集 d p [ i ] [ j ] = d p [ i − 1 ] [ 1 ] + 1 dp[i][j] = dp[i-1][1] + 1 dp[i][j]=dp[i1][1]+1

倒数第二个元素是 b [ j − 1 ] b[j-1] b[j1]​​子集 d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + 1 dp[i][j] = dp[i-1][j-1] + 1 dp[i][j]=dp[i1][j1]+1​​

所以初始化 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i-1][j] dp[i][j]=dp[i1][j]

如果 a [ i ] = b [ j ] a[i] = b[j] a[i]=b[j]​​ d p [ i ] [ j ] = m a x k = 1 j − 1 d p [ i − 1 ] [ k ] + 1 dp[i][j] = max_{k = 1} ^ {j-1}dp[i-1][k]+1 dp[i][j]=maxk=1j1dp[i1][k]+1

for (int i = 1; i <= n; i ++ )
{
    for (int j = 1; j <= n; j ++ )
    {
        f[i][j] = f[i - 1][j];
        if (a[i] == b[j])
        {
            int maxv = 1;
            for (int k = 1; k < j; k ++ )
                if (a[i] > b[k])
                    maxv = max(maxv, f[i - 1][k] + 1);
            f[i][j] = max(f[i][j], maxv);
        }
    }
}

此时需要 O ( n 3 ) O(n^3) O(n3)复杂度,需要进行优化

然后我们发现每次循环求得的 m a x v maxv maxv是满足 a [ i ] > b [ k ] a[i] > b[k] a[i]>b[k] f [ i − 1 ] [ k ] + 1 f[i - 1][k] + 1 f[i1][k]+1的前缀最大值。
因此可以直接将 m a x v maxv maxv提到第一层循环外面,减少重复计算,此时只剩下两重循环。

O ( n 2 ) O(n^2) O(n2)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
typedef long long ll;
const int M = 3010;
int n, a[M], b[M], dp[M][M];

int main()
{
    scanf("%d",&n);
    for(int i = 1;i <= n;i++) scanf("%d",&a[i]);
    for(int j = 1;j <= n;j++) scanf("%d",&b[j]);
    
    for(int i = 1;i <= n;i++)
    {
        int res = 1;
        for(int j = 1;j <= n;j++)
        {
            dp[i][j] = dp[i-1][j];
            if(a[i] == b[j]) dp[i][j] = res;
            if(a[i] > b[j]) res = max(res, dp[i][j] + 1);
        }
    }
    
    int ans = 1;
    for(int i = 1;i <= n;i++) ans = max(ans, dp[n][i]);
    printf("%d\n",ans);
    
    return 0;
}

背包模型

完全背包:求所有前缀的最大值

多重背包:求所有滑动窗口的最大值

423. 采药 - AcWing题库 (01背包)

标准01背包

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>

using namespace std;
const int M = 100;
int t, m;
int w[M], v[M], dp[1010];

int main()
{
    scanf("%d%d",&t,&m);
    for(int i = 1;i <= m;i++)   scanf("%d%d",&w[i],&v[i]);
    for(int i = 1;i <= m;i++)
    {
        for(int j = t;j >= w[i];j--)
        {
            dp[j] = max(dp[j], dp[j-w[i]] + v[i]) ;
        }
    }
    int ans = dp[t];
    printf("%d\n",ans);
    return 0;
}

1024. 装箱问题 - AcWing题库 (01背包改版)

初始化: d p [ n ] dp[n] dp[n]表示体积为n的背包最后剩下的空间

初始化: d p [ i ] = i dp[i] = i dp[i]=i

d p [ n ] = m i n ( d p [ n ] , d p [ n − a [ i ] ] ) dp[n] = min(dp[n], dp[n-a[i]]) dp[n]=min(dp[n],dp[na[i]])

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
int n, V;
int a[35], dp[20020];
int main()
{
    scanf("%d%d",&V,&n);
    for(int i = 1;i <= n;i++) scanf("%d",&a[i]);
    for(int i = 1;i <= V;i++) dp[i] = i;
    for(int i = 1;i <= n;i++)
    {
        for(int j = V;j >= a[i];j--)
        {
            dp[j] = min(dp[j], dp[j - a[i]]);
        }
    }
    printf("%d\n",dp[V]);
    return 0;
}

1022. 宠物小精灵之收服 - AcWing题库 (二维费用背包问题)

花费1:精灵球数量

花费2:皮卡丘体力值

价值:小精灵的数量

状态表示: f [ i , j , k ] f[i,j,k] f[i,j,k]表示只考虑前i个物品,费用1不超过j,费用2不超过k的最大价值

状态转移: f [ i , j , k ] = max ⁡ ( f [ i − 1 , j , k ] + f [ i − 1 , j − v 1 [ i ] , k − v 2 [ i ] ] + 1 ) f[i,j,k] = \max(f[i-1,j,k] + f[i-1,j-v1[i],k-v2[i]] + 1) f[i,j,k]=max(f[i1,j,k]+f[i1,jv1[i],kv2[i]]+1)

最多收服的小精灵数量: f [ K , N , M ] f[K,N,M] f[K,N,M]

最小耗费的体力值: m i n ( m ) min (m) min(m)​ 满足 f [ k , N , m ] = f [ k , N , M ] f[k,N,m] = f[k,N,M] f[k,N,m]=f[k,N,M]

可根据状态转移优化成一维

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
int n, m, k;
int v1[110], v2[110], f[1010][510];

int main()
{
    scanf("%d%d%d",&n, &m, &k);
    for(int i = 1;i <= k;i++)
    {
        scanf("%d%d", &v1[i], &v2[i]);
    }
    for(int i = 1;i <= k;i++)
    {
        for(int j = n;j >= v1[i];j--)
        {
            for(int x = m-1;x >= v2[i];x--)
            {
                f[j][x] = max(f[j][x], f[j-v1[i]][x-v2[i]] + 1);
            }
        }
    }
    int y = m;
    while(y > 0 && f[n][m-1] == f[n][y-1]) y--;
    printf("%d %d\n",f[n][m-1], m - y);
    return 0;
}

278. 数字组合 - AcWing题库(完全背包求数量)

看作01背包,容量为和m,每个物品价值 a [ i ] a[i] a[i]

初始化: d p [ 0 ] = 1 dp[0] = 1 dp[0]=1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-et8m1pcj-1629966810414)(C:/Users/DELL/AppData/Roaming/Typora/typora-user-images/image-20210728144904960.png)]

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
int n, m, a[110], dp[10010];

int main()
{
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++) {
        scanf("%d",&a[i]);
    }
    dp[0] = 1;
    for(int i = 1;i <= n;i++)
    {
        for(int j = m;j >= a[i];j--)
        {
            dp[j] += dp[j-a[i]];
        }
    }
    printf("%d\n",dp[m]);
    return 0;
}

1023. 买书 - AcWing题库(简单完全背包问题)

统计方案数

dp[0] = 1

dp[j] += dp[j-a[i]]

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
int n, a[4] = {10, 20, 50, 100}, dp[1010];

int main()
{
    scanf("%d",&n);
    dp[0] = 1;
    for(int i = 0;i <= 3;i++)
    {
        for(int j = a[i];j <= n;j++)
        {
            dp[j] += dp[j - a[i]];
        }
    }
    printf("%d\n",dp[n]);
    return 0;
}

1021. 货币系统 - AcWing题库(简单完全背包)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
int n, m;
int a[20];
long long dp[3010];

int main()
{
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++) scanf("%d", &a[i]);
    dp[0] = 1;
    for(int i = 1;i <= n;i++)
    {
        for(int j = a[i];j <= m;j++)
        {
            dp[j] += dp[j - a[i]];
        }
    }
    printf("%lld\n", dp[m]);
    return 0;
}

532. 货币系统 - AcWing题库(同上)

题意:给n种不同的货币,求问最小的m,使m种货币也能同时表示出n种货币能表示的,不能表示n种货币能表示的。

答案中的系统方案一定是由原先的系统方案去掉若干种货币得到

证明:

令给出的系统中的货币面值为 A 集合,需要得到的货币面值为 B 集合。

引理:A 集合中不能被其他数组成的数一定会在 B 集合中出现。

引理的证明:设有一个数 x∈A 且不能被 A 集合中其他数凑出来。 根据等价,如果 x∉B ,那么 B 中的其他数一定能组成 x .这就说明 B 中至少存在一个不属于 A 集合且不能被 A 组合出来的数(不然 A 集合就一定能合成 x ),那么这个数本身不属于 A 能组成的范畴,却属于 B 能组成的范畴,就不符合题意了。所以 x∈B ,引理正确性证毕。

那么现在我们需要证明:B⊆A.

仍然采用反证法。设存在一个数 x 满足 x∈B 且 x∉A .

根据题意,显然 x 能被 A 中若干个 a1,a2,…,ak组成(假定这些数不能被拆分成 A 中其他的数,如果能拆分就直接拿拆分方案替换即可)。根据引理,这些数都属于 B ,也就是说,B 完全可以通过这些数组成 x ,那么 B 中再存在一个 x 显然就是多余的,和 B 集合最小的要求不符。

那么只需在原先的基础之上去掉某些数

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
int T, n, a[110], dp[25010];

int main()
{
    scanf("%d",&T);
    while(T--)
    {
        memset(a,0,sizeof(a));
        memset(dp,0,sizeof(dp));
        dp[0] = 1;
        scanf("%d",&n);
        int maxn = 0;
        for(int i = 1;i <= n;i++) {
            scanf("%d",&a[i]);
            maxn = max(maxn, a[i]);
        }
        for(int i = 1;i <= n;i++)
        {
            for(int j = a[i];j <= maxn;j++)
            {
                dp[j] += dp[j-a[i]];
            }
        }
        int ans = 0;
        for(int i = 1;i <= n;i++)
        {
            if(dp[a[i]] == 1)   ans++;
        }
        printf("%d\n",ans);
    }
    return 0;
}

🔺6. 多重背包问题 III - AcWing题库(多重背包问题)(单调队列优化DP)

我们令 dp[j] 表示容量为j的情况下,获得的最大价值
那么,针对每一类物品 i ,我们都更新一下 dp[m] --> dp[0] 的值,最后 dp[m] 就是一个全局最优值

dp[m] = max(dp[m], dp[m-v] + w, dp[m-2*v] + 2*w, dp[m-3*v] + 3*w, ...)

接下来,我们把 dp[0] --> dp[m] 写成下面这种形式
dp[0], dp[v],   dp[2*v],   dp[3*v],   ... , dp[k*v]
dp[1], dp[v+1], dp[2*v+1], dp[3*v+1], ... , dp[k*v+1]
dp[2], dp[v+2], dp[2*v+2], dp[3*v+2], ... , dp[k*v+2]
...
dp[j], dp[v+j], dp[2*v+j], dp[3*v+j], ... , dp[k*v+j]

显而易见,m 一定等于 k*v + j,其中  0 <= j < v
所以,我们可以把 dp 数组分成 j 个类,每一类中的值,都是在同类之间转换得到的
也就是说,dp[k*v+j] 只依赖于 { dp[j], dp[v+j], dp[2*v+j], dp[3*v+j], ... , dp[k*v+j] }

因为我们需要的是{ dp[j], dp[v+j], dp[2*v+j], dp[3*v+j], ... , dp[k*v+j] } 中的最大值,
可以通过维护一个单调队列来得到结果。这样的话,问题就变成了 j 个单调队列的问题
    
所以,我们可以得到
dp[j]    =     dp[j]
dp[j+v]  = max(dp[j] +  w,  dp[j+v])
dp[j+2v] = max(dp[j] + 2w,  dp[j+v] +  w, dp[j+2v])
dp[j+3v] = max(dp[j] + 3w,  dp[j+v] + 2w, dp[j+2v] + w, dp[j+3v])
...
但是,这个队列中前面的数,每次都会增加一个 w ,所以我们需要做一些转换
    
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
    
单调队列问题,最重要的两点
1)维护队列元素的个数,如果不能继续入队,弹出队头元素
2)维护队列的单调性,即:尾值 >= dp[j + k*v] - k*w

本题中,队列中元素的个数应该为 s+1 个,即 0 -- s 个物品 i
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>

using namespace std;
const int MAXN = 20010;
int n, m;
int dp[MAXN], tmp[MAXN], q[MAXN];

int main()
{
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++)
    {
        int v, w, s;
        scanf("%d%d%d",&v,&w,&s);
        memcpy(tmp, dp, sizeof(dp));
        for(int j = 0;j < v;j++)
        {
            int hh = 0, tt = -1;
            for(int k = j;k <= m;k += v)
            {
                while(hh <= tt && q[hh] < k - s * v) hh++;
                while(hh <= tt && tmp[q[tt]] - (q[tt] - j) / v * w <= tmp[k] - (k - j) / v * w) tt--;
                q[++tt] = k;
                dp[k] = tmp[q[hh]] + (k - q[hh]) / v * w;
            }
        }
    }
    printf("%d\n",dp[m]);
    return 0;
}

1019. 庆功会 - AcWing题库(同上)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int MAXN = 6010;
int n, m;
int dp[MAXN], tmp[MAXN], q[MAXN];

int main()
{
    scanf("%d%d",&n,&m);
    for(int i = 0;i < n;i++)
    {
        int v, w, s;
        scanf("%d%d%d",&v,&w,&s);
        memcpy(tmp, dp, sizeof(dp));
        for(int j = 0;j < v;j++)
        {
            int hh = 0, tt = -1;
            for(int k = j;k <= m;k += v)
            {
                while(hh <= tt && k - s * v > q[hh]) hh++;
                while(hh <= tt && tmp[q[tt]] - (q[tt] - j) / v * w <= tmp[k] - (k - j) / v * w) tt--;
                q[++tt] = k;
                dp[k] = tmp[q[hh]] + (k - q[hh]) / v * w;
            }
        }
    }
    printf("%d\n",dp[m]);
    return 0;
}

7. 混合背包问题 - AcWing题库(混合背包问题)

01背包看作特殊的多重背包问题(采用二进制优化)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int M = 1010;
int N, V;
int dp[M];

int main()
{
    scanf("%d%d",&N,&V);
    for(int i = 1;i <= N;i++)
    {
        int v, w, s;
        scanf("%d%d%d",&v,&w,&s);
        if(s == 0)
        {
            for(int j = v;j <= V;j++)
            {
                dp[j] = max(dp[j], dp[j - v] + w);
            }
        }
        else
        {
            if(s == -1) s = 1;
            for(int k = 1; k <= s;k *= 2)
            {
                for(int j = V;j >= k * v;j--)
                {
                    dp[j] = max(dp[j], dp[j - k * v] + k * w);
                }
                s -= k;
            }
            if(s)
            {
                for(int j = V;j >= s * v;j--)
                {
                    dp[j] = max(dp[j], dp[j - s * v] + s * w);
                }
            }
        }
    }
    printf("%d\n", dp[V]);
    return 0;
}

8. 二维费用的背包问题 - AcWing题库(二维费用背包)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int MAXN = 101;
int N, V, M;
int dp[MAXN][MAXN];

int main()
{
    scanf("%d%d%d",&N,&V,&M);
    for(int i = 1;i <= N;i++)
    {
        int v, m, w;
        scanf("%d%d%d",&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);
            }
        }
    }
    printf("%d\n", dp[V][M]);
    return 0;
}

1020. 潜水员 - AcWing题库

f [ i ] [ j ] f[i][j] f[i][j] 表示氧气体积 ≥ i \ge i i 氮气体积 ≥ j \ge j j​​的最小瓶子体积。

循环的时候01背包需要枚举到0,然后取max

dp[i][j] = min(dp[i][j], dp[max(0, i - a)][max(0, j - b)] + c);

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int MAXN = 100;
int n, m;
int k;
int dp[MAXN][MAXN];

int main()
{
    scanf("%d%d",&m,&n);
    scanf("%d",&k);
    memset(dp, 0x3f, sizeof(dp));
    dp[0][0] = 0;
    while(k--)
    {
        int a, b, c;
        scanf("%d%d%d",&a,&b,&c);
        for(int i = m; i >= 0;i--)
        {
            for(int j = n;j >= 0;j--)
            {
                dp[i][j] = min(dp[i][j], dp[max(0, i - a)][max(0, j - b)] + c);
            }
        }
    }
    printf("%d\n", dp[m][n]);
    return 0;
}

1013. 机器分配 - AcWing题库

方法1:dfs
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
int maxx;
int n, m;
int a[12][20], cnt[13], tmp[13];

void dfs(int sum, int num, int res)	// dfs设置成void会更好,因为需要记录方案
{
    if(num == n)
    {
        sum += a[num][res];
        tmp[num] = res;
        if(maxx < sum)
        {
            maxx = sum;
            for(int i = 1;i <= n;i++) cnt[i] = tmp[i];
        }
        return;
    }
    for(int i = 0;i <= res;i++)	// 循环从0开始
    {
        tmp[num] = i;
        dfs(sum+a[num][i], num + 1, res - i);
    }
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++)
    {
        for(int j = 1;j <= m;j++)
        {
            scanf("%d",&a[i][j]);
        }
    }
    dfs(0, 1, m);
    printf("%d\n", maxx);
    for(int i = 1;i <= n;i++) printf("%d %d\n", i, cnt[i]);
    return 0;
}
方法2:分组背包

将公司看作组数n,拿物品的数量为体积,总体积为m。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;

const int N = 12, M = 17;
int n, m;
int a[N][M], way[N], dp[N][M];

int main()
{
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++)
    {
        for(int j = 1;j <= m;j++)
        {
            scanf("%d", &a[i][j]);
        }
    }
    
    for(int i = 1;i <= n;i++)
    {
        for(int j = 0;j <= m;j++)
        {
            dp[i][j] = dp[i-1][j];
            for(int k = 1;k <= j;k++)
            {
                dp[i][j] = max(dp[i][j], dp[i-1][j-k] + a[i][k]);
            }
        }
    }
    
    printf("%d\n", dp[n][m]);
    
    int j = m;
    for(int i = n;i >= 1;i--)
    {
        for(int k = 0;k <= j;k++)
        {
            if(dp[i][j] == dp[i-1][j-k] + a[i][k])
            {
                way[i] = k;
                j -= k;
                break;
            }
        }
    }
    
    for(int i = 1;i <= n;i++) printf("%d %d\n", i, way[i]);
    
    return 0;
}

426. 开心的金明 - AcWing题库(简单01背包)

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>

using namespace std;
int n, m;
int dp[30005];

int main()
{
    scanf("%d%d", &n, &m);
    for(int i = 1;i <= m;i++)
    {
        int v, p;
        scanf("%d%d", &v, &p);
        for(int j = n;j >= v;j--)
        {
            dp[j] = max(dp[j], dp[j - v] + v * p);
        }
    }
    printf("%d\n", dp[n]);
    return 0;
}

🔺10. 有依赖的背包问题 - AcWing题库(树形dp)

d p [ u ] [ k ] dp[u][k] dp[u][k]表示以u为根节点,体积剩余k的最大价值

p为u的子节点 (采用分组背包,先枚举体积,再枚举决策)

dp[u][j] = max(dp[u][j], dp[u][j - k] + dp[p][k])

递归结束再将根节点加进去

image-20210804111437684

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QxvtmH5G-1629966810417)(C:/Users/DELL/AppData/Roaming/Typora/typora-user-images/image-20210804111511179.png)]

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int MAXN = 105;
struct node{
    int to, next;
}N[MAXN];

int head[MAXN], dp[MAXN][MAXN], id, v[MAXN], w[MAXN], p;
int n, V;

inline void addedge(int u, int v)
{
    N[id].to = v;
    N[id].next = head[u];
    head[u] = id++;
    return;
}

void dfs(int u)
{
    for(int i = head[u]; i != -1; i = N[i].next)
    {
        int p = N[i].to;
        dfs(p);
        
        // 分组背包
        for(int j = V - v[u]; j >= 0; j--)  // 枚举体积
        {
            for(int k = 0;k <= j;k++)   // 枚举决策
            {
                dp[u][j] = max(dp[u][j], dp[u][j - k] + dp[p][k]);
            }
        }
    }
    
    // 加入物品u
    for(int i = V;i >= v[u];i--) dp[u][i] = dp[u][i - v[u]] + w[u];
    for(int i = 0;i < v[u];i++) dp[u][i] = 0;
}

int main()
{
    scanf("%d%d",&n, &V);
    memset(head, -1, sizeof(head));
    int root;
    for(int i = 1;i <= n;i++)
    {
        scanf("%d%d%d", &v[i], &w[i], &p);
        if(p == -1) root = i;
        addedge(p, i);
    }
    dfs(root);
    printf("%d\n", dp[root][V]);
    return 0;
}

11. 背包问题求方案数 - AcWing题库

g [ i ] [ j ] g[i][j] g[i][j] 表示当 f [ i ] [ j ] f[i][j] f[i][j]取到最大值的时候的方案数

if(f[i][j] == f[i-1][j]) g[i][j] = g[i-1][j]

if(f[i][j] == f[i-1][j - v[i]] + w[i]) g[i][j] += g[i-1][j-v[i]]

可以优化成为一维的,只需统计 j = V j = V j=V时, g [ i ] [ j ] g[i][j] g[i][j]的数量

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>

using namespace std;
typedef long long ll;
const int mod = 1e9+7;
const int MAXN = 1005;
ll N, V, v[MAXN], w[MAXN], dp[MAXN], g[MAXN];

int main()
{
    scanf("%d%d", &N, &V);
    for(int i = 0;i < N;i++)
    {
        scanf("%d%d", &v[i], &w[i]);
    }
    g[0] = 1;
    for(int i = 0;i < N;i++)
    {
        for(int j = V;j >= v[i];j--)
        {
            ll cnt = 0;
            int maxx = max(dp[j], dp[j - v[i]] + w[i]);
            if(maxx == dp[j]) cnt = g[j];
            if(maxx == dp[j - v[i]] + w[i]) cnt = (cnt + g[j - v[i]]) % mod;
            g[j] = cnt;
            dp[j] = maxx;
        }
    }
    
    ll maxn = 0, res = 0;
    for(int i = 0;i <= V;i++) maxn = max(maxn, dp[i]);
    
    for(int i = 0;i <= V;i++)
    {
        if(maxn == dp[i]) res = (res + g[i]) % mod;
    }
    
    printf("%lld\n", res);
    return 0;
}

12. 背包问题求具体方案 - AcWing题库

逆序更新dp数组,保证最后一次被更新的一定是最小的

贪心选择,从小到大枚举物品,如果当前物品可选可不选,则必须选,能保证字典序最小

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int MAXN = 1005;
int N, V;
int dp[MAXN][MAXN], v[MAXN], w[MAXN];

int main()
{
    scanf("%d%d", &N, &V);
    for(int i = 1;i <= N;i++) scanf("%d%d", &v[i], &w[i]);
    for(int i = N;i >= 1;i--) // 从大到小进行枚举,保证最后一次被更新的一定是最小的
    {
        for(int j = 0;j <= V;j++)
        {
            dp[i][j] = dp[i + 1][j]; 
            if(j >= v[i]) dp[i][j] = max(dp[i][j], dp[i + 1][j - v[i]] + w[i]);
        }
    }
    
    int j = V;
    for(int i = 1;i <= N;i++)	// 从小到大寻找物品
    {
        if(j >= v[i] && dp[i][j] == dp[i + 1][j - v[i]] + w[i])
        {
            printf("%d ", i);
            j -= v[i];
        }
    }
    printf("\n");
    
    return 0;
}

🔺734. 能量石 - AcWing题库(贪心+DP)

题意:n个能量石,吃下去需要一定时间,同时每一秒钟会流失一定能量,问最大能吃的能量是多少

贪心:对于两个相邻要吃的 i , j i,j i,j来说

先i后j: E i + E j − L j ∗ S i E_i+E_j-L_j*S_i Ei+EjLjSi

先j后i: E i + E j − L i ∗ S j E_i+E_j-L_i*S_j Ei+EjLiSj

能量最大为 L j ∗ S i < L i ∗ S j L_j*S_i<L_i*S_j LjSi<LiSj

所以顺序已定,只需考虑要吃哪些。

DP

吃前i个物品,且时间不超过j

d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 , j − s ] + e − ( j − s ) ∗ l ) dp[i][j] = max(dp[i-1][j], dp[i-1,j-s]+e-(j-s)*l) dp[i][j]=max(dp[i1][j],dp[i1,js]+e(js)l)

总时间为所有时间之和。可优化为1维

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int MAXN = 10010;
int T, n;

struct node{
    int s, e, l;
    bool operator<(const node &a)
    {
        return s * a.l < l * a.s;
    }
}N[MAXN];

int dp[MAXN];

int main()
{
    scanf("%d", &T);
    for(int x = 1;x <= T;x++)
    {
        memset(dp,0,sizeof(dp));
        int m = 0;
        scanf("%d", &n);
        for(int i = 1;i <= n;i++)
        {
            int s, e, l;
            scanf("%d%d%d", &s, &e, &l);
            N[i] = {s, e, l};
            m += s;
        }
        sort(N+1, N+n+1);
        /*
        for(int i = 1;i <= n;i++)
        {
            printf("%d %d %d\n", N[i].s, N[i].e, N[i].l);
        }
        */
        for(int i = 1;i <= n;i++)
        {
            for(int j = m;j >= N[i].s;j--)
            {
                dp[j] = max(dp[j], dp[j - N[i].s] + max(0, N[i].e - (j - N[i].s) * N[i].l));
            }
        }
        int maxn = 0;
        for(int i = 1;i <= m;i++) maxn = max(maxn, dp[i]);
        printf("Case #%d: %d\n", x, maxn);
    }
    return 0;
}

🔺487. 金明的预算方案 - AcWing题库(分组背包)

可以将每个主件及其附件看作一个物品组,记主件为p,两个附件为a,b,则最多一共有4种组合:

1.p
2.p,a
3.p,b
4.p,a,b

这四种组合是互斥的,最多只能从中选一种,因此可以将每种组合看作一个物品,那么问题就变成了分组背包问题。

分组背包的时间复杂度是 物品总数 * 总体积,因此总时间复杂度是 O ( N m ) O(Nm) O(Nm)

在枚举四种组合时可以使用二进制的思想,可以简化代码。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>

using namespace std;
typedef pair<int, int> PII;
const int MAXN = 32010;
PII master[MAXN];
vector<PII>servent[MAXN];
int n, m;
int dp[MAXN];

int main()
{
    cin >> n >> m;
    for(int i = 1;i <= m;i++)
    {
        int v, p, q;
        cin >> v >> p >> q;
        if(!q) master[i] = {v, v * p};
        else servent[q].push_back({v, v * p});
    }
    
    for(int i = 1;i <= m;i++)
    {
        if(master[i].first)
        {
            for(int j = n;j >= 0;j--)
            {
                for(int k = 0;k < 1 << servent[i].size();k++)
                {
                    int v = master[i].first, w = master[i].second;
                    {
                        for(int x = 0;x < servent[i].size();x++)
                        {
                            if(k >> x & 1)
                            {
                                v += servent[i][x].first;
                                w += servent[i][x].second;
                            }
                        }
                    }
                    if(j >= v) dp[j] = max(dp[j], dp[j - v] + w);
                }
            }
        }
    }
    
    printf("%d\n", dp[n]);
    
    return 0;
}

状态机模型

1049. 大盗阿福 - AcWing题库(简单状态机)

dp[i][0] = max(dp[i - 1][1], dp[i - 1][0]);
dp[i][1] = dp[i - 1][0] + a[i];

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int MAXN = 1e5+50;
int T, n;
int a[MAXN], dp[MAXN][2];

int main()
{
    cin >> T;
    while(T--)
    {
        scanf("%d", &n);
        for(int i = 1;i <= n;i++) scanf("%d", &a[i]);
        memset(dp, 0, sizeof(dp));
        for(int i = 1;i <= n;i++)
        {
            dp[i][0] = max(dp[i - 1][1], dp[i - 1][0]);
            dp[i][1] = dp[i - 1][0] + a[i];
        }
        int ans = max(dp[n][0], dp[n][1]);
        printf("%d\n", ans);
    }
    return 0;
}

1057. 股票买卖 IV - AcWing题库

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6i3xyFuR-1629966810419)(C:/Users/DELL/AppData/Roaming/Typora/typora-user-images/image-20210813105924320.png)]

关于为什么是第一次买的时候交易次数加一,而不是卖的时候?

终究要回归到状态转移的起点,第一支股票只有买,和不买这两个操作,一定不可能是卖和不卖的这两个操作,因此第一支股票如果买入时,必须按照一次交易处理。否则如果第一次股票如果买入时,不按一次交易处理,也就代表着第一支股票卖出才算一次交易,也就代表着在第一支股票卖出之前还买了一支股票,显然是矛盾的。

记住进行空间优化

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
typedef long long ll;
const int MAXN = 1e5 + 50;
int n, k;
int a[MAXN], dp[105][2];

int main()
{
    scanf("%d%d", &n, &k);
    for(int i = 1;i <= n;i++) scanf("%d", &a[i]);
    memset(dp, -0x3f, sizeof(dp));
    dp[0][0] = 0;
    
    for(int i = 1;i <= n;i++)
    {
        for(int j = k;j >= 1;j--)
        {
            dp[j][0] = max(dp[j][0], dp[j][1] + a[i]);
            dp[j][1] = max(dp[j][1], dp[j - 1][0] - a[i]);
        }
    }
    int maxn = dp[0][0];
    for(int i = 0;i <= k;i++) maxn = max(maxn, dp[i][0]);
    printf("%d\n", maxn);
    return 0;
}

1058. 股票买卖 V - AcWing题库

d p [ i ] [ j ] dp[i][j] dp[i][j]为第 i i i​天,状态为 j j j的情况

d p [ i ] [ 0 ] dp[i][0] dp[i][0]​表示空仓情况:dp[i][0] = max(dp[i-1][2], dp[i-1][0])

d p [ i ] [ 1 ] dp[i][1] dp[i][1]表示手中有股票:dp[i][1] = max(dp[i-1][1], dp[i-1][0] - a[i])

d p [ i ] [ 2 ] dp[i][2] dp[i][2]表示冷冻期:dp[i][2] = dp[i-1][1] + a[i]

初始化: d p [ i ] [ j ] = − i n f , d p [ 0 ] [ 0 ] = 0 dp[i][j]=-inf,dp[0][0] = 0 dp[i][j]=inf,dp[0][0]=0

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace st
typedef long long ll;
const int MAXN = 1e5+50;
int n;
ll a[MAXN], dp[MAXN][3];

int main()
{
    scanf("%d", &n);
    for(int i = 1;i <= n;i++) scanf("%lld", &a[i]);
    memset(dp, -0x3f, sizeof(dp));
    dp[0][0] = 0;
    
    for(int i = 1;i <= n;i++)
    {
        dp[i][0] = max(dp[i - 1][0], dp[i - 1][2]);
        dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - a[i]);
        dp[i][2] = dp[i - 1][1] + a[i];
    }
    
    printf("%lld\n", max(dp[n][0], dp[n][2]));
    
    return 0;
}

状压DP

1064. 小国王 - AcWing题库

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rYtBZ7ns-1629966810420)(C:/Users/DELL/AppData/Roaming/Typora/typora-user-images/image-20210813191820295.png)]

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>

using namespace std;
typedef long long ll;
const int M = 1 << 10, N = 13, K = 110;
int n, k;
int cnt[M];
vector<int>head[M];
vector<int>state;
ll f[N][K][M];

bool check(int x)
{
    for(int i = 0;i < n+1;i++)
    {
        if(x >> i & 1 && x >> (i + 1) & 1) return false;
    }
    return true;
}

int count(int x)
{
    int res = 0;
    for(int i = 0;i < n+1;i++)
    {
        if(x >> i & 1) res++;
    }
    return res;
}

int main()
{
    scanf("%d%d", &n, &k);
    
    for(int i = 0;i < 1 << n;i++)
    {
        if(check(i))
        {
            state.push_back(i);
            cnt[i] = count(i);
        }
    }
    
    for(int i = 0;i < state.size();i++)
    {
        for(int j = 0;j < state.size();j++)
        {
            int a = state[i], b = state[j];
            if((a & b) == 0 && check(a | b)) head[i].push_back(j);
        }
    }
    
    f[0][0][0] = 1;
    
    for(int i = 1;i <= n+1;i++)
    {
        for(int j = 0;j <= k;j++)
        {
            for(int a = 0;a < state.size();a++)
            {
                for(auto b : head[a])
                {
                    int c = cnt[state[a]];
                    if(j >= c) f[i][j][a] += f[i - 1][j - c][b];
                }
            }
        }
    }
    
    printf("%lld\n", f[n+1][k][0]);
    
    return 0;
}

327. 玉米田 - AcWing题库

dp[i][j] += dp[i-1][k]

需要判断当前能否种玉米,不能有两个连续的1,上下满足(a & b) == 0

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>

using namespace std;
typedef long long ll;
const int mod = 1e8, M = 13;
int n, m;
int a[M][M];
ll dp[M][1 << M];
vector<int>state;
vector<int>head[1 << M];

bool check(int x)
{
    for(int i = 0;i < n;i++)
    {
        if(x >> i & 1 && x >> (i + 1) & 1) return false;
    }
    return true;
}

bool checka(int x, int m)
{
    for(int i = 1;i <= n;i++)
    {
        if(a[m][i] == 0 && x >> (n - i) & 1) return false;
    }
    return true;
}

int main()
{
    scanf("%d%d", &m, &n);
    for(int i = 1;i <= m;i++)
    {
        for(int j = 1;j <= n;j++)
        {
            scanf("%d", &a[i][j]);
        }
    }
    
    for(int i = 0;i < 1 << n;i++)
    {
        if(check(i))
        {
            state.push_back(i);
        }
    }
    
    for(int i = 0;i < state.size();i++)
    {
        for(int j = 0;j < state.size();j++)
        {
            int a = state[i], b = state[j];
            if((a & b) == 0) head[i].push_back(j);
        }
    }
    
    dp[0][0] = 1;
    for(int i = 1;i <= m;i++)
    {
        for(int j = 0;j < state.size();j++)
        {
            for(auto k : head[j])
            {
                if(checka(state[j], i)) dp[i][j] = (dp[i][j] + dp[i - 1][k]) % mod;
            }
        }
    }
    
    ll ans = 0;
    for(int i = 0;i < 1 << n;i++) ans = (ans + dp[m][i]) % mod;
    printf("%lld\n", ans);
    
    return 0;
}

292. 炮兵阵地 - AcWing题库

dp[i][j][k]表示第i行,状态为j,第 i − 1 i-1 i1行状态为k

dp[i][j][k] = max(dp[i][j][k], dp[i-1][k][x] + cnt(第i行状态为j,1的数量))

满足连续三行状态 a , b , c a,b,c a,b,c两两相与都为0,并且高地上不能放

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
typedef long long ll;
const int N = 105, M = 15, MAXN = 1 << 10;
int n, m;
int cnt[MAXN];
ll dp[2][MAXN][MAXN];
vector<int>state;
char s[N][M];

int count(int x)
{
    int res = 0;
    for(int i = 0;i < m;i++)
    {
        if(x >> i & 1) res++;
    }
    return res;
}

bool check(int x)
{
    for(int i = 0;i < m;i++)
    {
        if(x >> i & 1 && (x >> (i + 1) & 1 || x >> (i + 2) & 1)) return false;
    }
    return true;
}

bool checka(int x, int n)
{
    for(int i = 0;i < m;i++)
    {
        if(s[n][m-i] == 'H' && x >> i & 1) return false;
    }
    return true;
}

int main()
{
    scanf("%d%d", &n, &m);
    for(int i = 1;i <= n;i++)
    {
        for(int j = 1;j <= m;j++)
        {
            char c;
            cin >> c;	// 注意采用cin读入,用getchar可能没法忽略空格
            s[i][j] = c;
        }
    }
    
    for(int i = 0;i < 1 << m;i++)
    {
        if(check(i))
        {
            state.push_back(i);
            cnt[i] = count(i);
        }
    }
    
    for(int i = 1;i <= n;i++)
    {
        for(int j = 0;j < state.size();j++)
        {
            for(int k = 0;k < state.size();k++)
            {
                for(int x = 0; x < state.size();x++)
                {
                    int a = state[j], b = state[k], c = state[x];
                    if((a & b) || (b & c) || (a & c)) continue;
                    if(!checka(a, i)) continue;
                    dp[i & 1][j][k] = max(dp[i & 1][j][k], dp[(i - 1) & 1][k][x] + cnt[a]);
                }
            }
        }
    }
    
    ll res = 0;
    for(int i = 0;i < state.size();i++)
    {
        for(int j = 0;j < state.size();j++)
        {
            res = max(res, dp[n & 1][i][j]);
        }
    }
    printf("%lld\n", res);
    return 0;
}

524. 愤怒的小鸟 - AcWing题库

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yCpev2f6-1629966810420)(C:/Users/DELL/AppData/Roaming/Typora/typora-user-images/image-20210816114734470.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TpC97xP3-1629966810421)(C:/Users/DELL/AppData/Roaming/Typora/typora-user-images/image-20210816114855394.png)]

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#include<cmath>

using namespace std;
typedef pair<double, double> PDD;
const int M = 20;
const double eps = 1e-7;
int T, n, m;
PDD state[M];
int f[1 << M];
int path[M][M];

int cmp(double x, double y)
{
    if(fabs(x - y) < eps)  return 0;
    else if(x > y) return 1;
    else return -1;
}

void init()
{
    memset(path, 0, sizeof(path));
    for(int i = 0;i < n;i++)
    {
        path[i][i] = 1 << i;
        for(int j = 0;j < n;j++)
        {
            double x1 = state[i].first, y1 = state[i].second;
            double x2 = state[j].first, y2 = state[j].second;
            if(cmp(x1, x2) == 0) continue;
            double a = (y1 * x2 - y2 * x1) / (x1 * x2 *(x1 - x2));
            double b = (y1 / x1) - a * x1;
            if(cmp(a, 0.0) >= 0) continue;
            
            for(int k = 0;k < n;k++)
            {
                double x = state[k].first, y = state[k].second;
                if(cmp(y, a * x * x + b * x) == 0) path[i][j] += 1 << k;
            }
        }
    }
}

int main()
{
    scanf("%d", &T);
    while(T--)
    {
        scanf("%d%d", &n, &m);
        for(int i = 0;i < n;i++)
        {
            double x, y;
            scanf("%lf%lf", &x, &y);
            state[i].first = x;
            state[i].second = y;
        }
        memset(f, 0x3f, sizeof(f));
        f[0] = 0;
        init();
        for(int i = 0;i < (1 << n);i++)
        {
            int t = -1;
            for(int j = 0;j < n;j++)
            {
                if((i >> j & 1) == 0) t = j;
            }
            if(t != -1)
            {
                for(int j = 0;j < n;j++)
                {
                    f[i | path[t][j]] = min(f[i | path[t][j]], f[i] + 1);
                }
            }
        }
        printf("%d\n", f[(1 << n) - 1]);
    }
    return 0;
}

区间DP

代码实现

迭代式:
// 先枚举长度,再枚举左端点,求出右端点
for(int len = 1;len <= n;len++)
{
    for(int l = 1;l + len - 1 <= n;i++)
    {
        int r = l + len - 1;
    }
}
记忆化搜索:

1068. 环形石子合并 - AcWing题库(环转化为区间)

环转化为区间:

  • 方法一:枚举缺口,拉成直线,一共n种情况。
  • 方法二:答案最终为n条长度为n的链的所有情况,将区间拉成 2 n 2n 2n,每个区间为 [ i , i + n − 1 ] [i,i+n-1] [i,i+n1]
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cstdio>

using namespace std;
typedef long long ll;
const int M = 220;
int n;
int a[2*M], f[2*M][2*M], g[2*M][2*M], sum[2 * M];

int main()
{
    scanf("%d", &n);
    for(int i = 1;i <= n;i++) 
    {
        scanf("%d", &a[i]);
        a[n + i] = a[i];
    }
    
    memset(f, 0x3f, sizeof(f));
    memset(g, -0x3f, sizeof(g));
    
    
    for(int i = 1;i <= 2 * n;i++)
    {
        sum[i] = sum[i - 1] + a[i];
    }
    
    for(int len = 1;len <= 2 * n;len++)
    {
        for(int l = 1;l + len - 1 <= 2 * n;l++)
        {
            int r = l + len - 1;
            if(l == r) {
                f[l][r] = 0;
                g[l][r] = 0;
                continue;
            }
            for(int k = l;k <= r;k++)
            {
                f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + (sum[r] - sum[l - 1]));
                g[l][r] = max(g[l][r], g[l][k] + g[k + 1][r] + (sum[r] - sum[l - 1]));
            }
        }
    }
    
    int minn = 1e9, maxx = 0;
    for(int i = 1;i <= n;i++)
    {
        minn = min(minn, f[i][i + n - 1]);
        maxx = max(maxx, g[i][i + n - 1]);
    }
    
    printf("%d\n%d\n", minn, maxx);
    
    return 0;
}

320. 能量项链 - AcWing题库

环形开两倍长度转化为线性的

f[l][r] = f[l][k] + f[k][r] + a[l] * a[k] * a[r] l < k < r

答案为 m a x ( f [ i ] [ i + n ] ) max(f[i][i+n]) max(f[i][i+n])

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int M = 220;
int n;
int a[M], f[M][M];

int main()
{
    scanf("%d", &n);
    for(int i = 1;i <= n;i++)
    {
        scanf("%d", &a[i]);
        a[n + i] = a[i];
    }
    
    for(int len = 3;len <= 2 * n;len++)
    {
        for(int l = 1;l + len - 1 <= 2 * n;l++)
        {
            int r = l + len - 1;
            for(int k = l + 1;k < r;k++)
            {
                f[l][r] = max(f[l][r], f[l][k] + f[k][r] + a[l] * a[k] * a[r]);
            }
        }
    }
    
    int maxx = 0;
    for(int i = 1;i <= n;i++)
    {
        maxx = max(maxx, f[i][i + n]);
    }
    
    printf("%d\n", maxx);
    
    return 0;
}

479. 加分二叉树 - AcWing题库(dfs+区间dp)

f[l][r] = left * right + a[r]

if(l == r) ans = a[l]

if(k == l) left = 1 else left = f[l][k - 1]

if(k == r) right = 1 else right = f[k + 1][r]

同时记住每次更新的根,dfs进行前序遍历

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int M = 32;
int n;
int a[M];
unsigned f[M][M];
int root[M][M];

void dfs(int l, int r)
{
    if(l > r) return;
    int k = root[l][r];
    printf("%d ", k);
    dfs(l, k - 1);
    dfs(k + 1, r);
}

int main()
{
    scanf("%d", &n);
    for(int i = 1;i <= n;i++) {
        scanf("%d", &a[i]);
    }
    for(int len = 1;len <= n;len++)
    {
        for(int l = 1;l + len - 1 <= n;l++)
        {
            int r = l + len - 1;
            for(int k = l;k <= r;k++)
            {
                int left = k == l ? 1 : f[l][k - 1];
                int right = k == r ? 1 : f[k + 1][r];
                int ans = left * right + a[k];
                if(l == r) ans = a[l];
                if(f[l][r] < ans)
                {
                    f[l][r] = ans;
                    root[l][r] = k;
                }
            }
        }
    }
    printf("%d\n", f[1][n]);
    dfs(1, n);
    printf("\n");
    return 0;
}

1069. 凸多边形的划分 - AcWing题库(三角剖分)

f[i][j] = min(f[i][k] + f[k][j] + a[i] * a[k] * a[j]) l < k < r

答案为: f [ 1 ] [ n ] f[1][n] f[1][n]

高精度

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 55, M = 35, INF = 1e9;

int n;
int w[N];
LL f[N][N][M];

void add(LL a[], LL b[])
{
    static LL c[M];
    memset(c, 0, sizeof c);
    for (int i = 0, t = 0; i < M; i ++ )
    {
        t += a[i] + b[i];
        c[i] = t % 10;
        t /= 10;
    }
    memcpy(a, c, sizeof c);
}

void mul(LL a[], LL b)
{
    static LL c[M];
    memset(c, 0, sizeof c);
    LL t = 0;
    for (int i = 0; i < M; i ++ )
    {
        t += a[i] * b;
        c[i] = t % 10;
        t /= 10;
    }
    memcpy(a, c, sizeof c);
}

int cmp(LL a[], LL b[])
{
    for (int i = M - 1; i >= 0; i -- )
        if (a[i] > b[i]) return 1;
        else if (a[i] < b[i]) return -1;
    return 0;
}

void print(LL a[])
{
    int k = M - 1;
    while (k && !a[k]) k -- ;
    while (k >= 0) cout << a[k -- ];
    cout << endl;
}

int main()
{
    cin >> n;
    for (int i = 1; i <= n; i ++ ) cin >> w[i];

    LL temp[M];
    for (int len = 3; len <= n; len ++ )
        for (int l = 1; l + len - 1 <= n; l ++ )
        {
            int r = l + len - 1;
            f[l][r][M - 1] = 1;
            for (int k = l + 1; k < r; k ++ )
            {
                memset(temp, 0, sizeof temp);
                temp[0] = w[l];
                mul(temp, w[k]);
                mul(temp, w[r]);
                add(temp, f[l][k]);
                add(temp, f[k][r]);
                if (cmp(f[l][r], temp) > 0)
                    memcpy(f[l][r], temp, sizeof temp);
            }
        }

    print(f[1][n]);

    return 0;
}

321. 棋盘分割 - AcWing题库

题意:将8*8的棋盘进行分割,使得分割n次的方差最小,每次分割只能在原棋盘割下一块矩形的剩下部分进行分割。
σ 2 = 1 n ∑ i = 1 n ( x i − x ) 2 σ 2 = 1 n ∑ i = 1 n ( x i 2 − 2 x i x + x 2 ) σ 2 = 1 n ∑ i = 1 n ( x i 2 − x 2 ) \sigma^2 = \frac{1}{n}\sum_{i=1}^{n}(x_i-x)^2 \\ \sigma^2 = \frac{1}{n} \sum_{i=1}^{n}(x_i^2 -2 x_ix+x^2) \\ \sigma^2 = \frac{1}{n} \sum_{i=1}^{n}(x_i^2-x^2) σ2=n1i=1n(xix)2σ2=n1i=1n(xi22xix+x2)σ2=n1i=1n(xi2x2)
为了使 σ \sigma σ越小,只需要让每一块的平方和越小

POJ1191.jpg

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>

using namespace std;
const int M = 9, N = 15;
int n;
int s[M][M];
double f[M][M][M][M][N];
double X;

int sum(int x1, int y1, int x2, int y2)
{
    int ans = s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1];
    return ans;
}

double get_sum(int x1, int y1, int x2, int y2)
{
    double res = (double)sum(x1, y1, x2, y2) - X;
    return res * res / n;
}

double dp(int x1, int y1, int x2, int y2, int k)
{
    auto &u = f[x1][y1][x2][y2][k];
    if(u >= 0) return u;
    if(k == 1) return u = get_sum(x1, y1, x2, y2);
    
    u = 1e9;
    for(int a = x1; a < x2; a++)
    {
        u = min(u, get_sum(x1, y1, a, y2) + dp(a + 1, y1, x2, y2, k - 1));
        u = min(u, get_sum(a + 1, y1, x2, y2) + dp(x1, y1, a, y2, k - 1));
    }
    
    for(int b = y1; b < y2; b++)
    {
        u = min(u, get_sum(x1, y1, x2, b) + dp(x1, b + 1, x2, y2, k - 1));
        u = min(u, get_sum(x1, b + 1, x2, y2) + dp(x1, y1, x2, b, k - 1));
    }
    
    return u;
}

int main()
{
    scanf("%d", &n);
    for(int i = 1;i <= 8;i++)
    {
        for(int j = 1;j <= 8;j++)
        {
            scanf("%d", &s[i][j]);
            s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
        }
    }
    
    X = (double)s[8][8] / n;
    memset(f, -1, sizeof(f));
    double ans = sqrt(dp(1, 1, 8, 8, n));
    printf("%.3f\n", ans);
    
    return 0;
}

树形DP

1072. 树的最长路径 - AcWing题库(模板题)

树的最长路径.jpg

f [ u ] = f [ v ] v ∈ s o n [ u ] + w [ u , v ] f[u] = f[v]_{v ∈ son[u]}+w[u,v] f[u]=f[v]vson[u]+w[u,v]

答案为两条最长链之和(用dfs去写)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int N = 10010, M = 10010 * 2;

struct Edge{
    int to, next, w;
}E[M];

int head[N];
int id, n, ans;

void inline addedge(int u, int v, int w)
{
    E[id].to = v;
    E[id].next = head[u];
    E[id].w = w;
    head[u] = id++;
    return ;
}

int dfs(int cur, int father)
{
    int dist = 0, d1 = 0, d2 = 0;
    
    for(int i = head[cur];i != -1;i = E[i].next)
    {
        int p = E[i].to, w = E[i].w;
        if(p == father) continue;
        int d = dfs(p, cur) + w;
        dist = max(dist, d);
        
        if(d >= d1) {
            d2 = d1;
            d1 = d;
        }
        else if(d > d2) {
            d2 = d;
        }
        
    }
    ans = max(ans, d1 + d2);
    
    return dist;
}

int main()
{
    memset(head, -1, sizeof(head));
    scanf("%d", &n);
    for(int i = 1;i < n;i++)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        addedge(a, b, c);
        addedge(b, a, c);
    }
    
    dfs(1, -1);
    printf("%d\n", ans);
    
    return 0;
}

1073. 树的中心 - AcWing题库

树的中心.jpg

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int M = 20020, N = 10010;

int head[N], d1[N], d2[N], up[N], p1[N], id, n;
bool is_leaf[N];
struct Edge{
    int to, next, w;
}E[M];

inline void addedge(int u, int v, int w)
{
    E[id].to = v;
    E[id].w = w;
    E[id].next = head[u];
    head[u] = id++;
    return;
}

int dfs_d(int cur, int father)	// 向下走保存向下的最大和次大
{
    d1[cur] = -1e9, d2[cur] = -1e9;
    for(int i = head[cur]; i != -1; i = E[i].next)
    {
        int p = E[i].to, w = E[i].w;
        if(p == father) continue;
        int d = dfs_d(p, cur) + w;
        if(d >= d1[cur]) {
            d2[cur] = d1[cur];
            d1[cur] = d;
            p1[cur] = p;
        }
        else if(d > d2[cur]) d2[cur] = d;
    }
    if(d1[cur] == -1e9) {
        is_leaf[cur] = true;
        d1[cur] = d2[cur] = 0;
    }
    return d1[cur];
}

void dfs_u(int cur, int father)	// 向上走找父节点的的(最大/次大),或者再往上走
{
    for(int i = head[cur]; i != -1; i = E[i].next)
    {
        int p = E[i].to, w = E[i].w;
        if(p == father) continue;
        
        if(p1[cur] == p) up[p] = max(up[cur], d2[cur]) + w;
        else up[p] = max(up[cur], d1[cur]) + w;
        
        dfs_u(p, cur);
    }
}

int main()
{
    memset(head, -1, sizeof(head));
    memset(is_leaf, false, sizeof(is_leaf));
    scanf("%d", &n);
    for(int i = 1;i < n;i++)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        addedge(a, b, c);
        addedge(b, a, c);
    }
    
    dfs_d(1, -1);
    dfs_u(1, -1);
    
    int ans = d1[1];
    for(int i = 1;i <= n;i++)
    {
        if(is_leaf[i]) ans = min(ans, up[i]);
        else ans = min(ans, max(up[i], d1[i]));
    }
    printf("%d\n", ans);
    return 0;
}

1075. 数字转换 - AcWing题库

一个数它的约数之和小于它的话,将其连一条无向边

每次的终点都是1

即只需要找出从一开始找出两条最长的链(树形dp模板题)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int N = 50005, M = 2 * N;
int n, id, ans;
int head[N];
bool st[N];
struct Edge{
    int to, next;
}E[M];

int getsum(int x)
{
    if(x == 1) return -1;
    int res = 1;
    for(int i = 2;i * i <= x;i++)
    {
        if(x % i == 0)
        {
            res += i;
            if(i * i != x) res += (x / i);
        }
    }
    if(res < x) return res;
    else return -1;
}

inline void addedge(int u, int v)
{
    E[id].to = v;
    E[id].next = head[u];
    head[u] = id++;
    return ;
}

int dfs(int cur, int father)
{
    int dist = 0, d1 = 0, d2 = 0;
    for(int i = head[cur]; i != -1; i = E[i].next)
    {
        int p = E[i].to;
        if(p == father) continue;
        int d = dfs(p, cur) + 1;
        dist = max(dist, d);
        if(d >= d1) {
            d2 = d1;
            d1 = d;
        }
        else if(d > d2) d2 = d;
        ans = max(ans, d1 + d2);
    }
    return dist;
}

int main()
{
    memset(head, -1, sizeof(head));
    scanf("%d", &n);
    for(int i = 1;i <= n;i++)
    {
        int tmp = getsum(i);
        if(tmp != -1) {
            addedge(i, tmp);
            addedge(tmp, i);
        }
    }
    dfs(1, -1);
    printf("%d\n", ans);

    return 0;
}

1074. 二叉苹果树 - AcWing题库(有依赖的背包类树形dp)

总结:

一:状态表示:

考虑根节点和孩子之间的关系, d p [ i ] [ j ] dp[i][j] dp[i][j] 表示以 i i i为根分配 j j j那么大的体积的最大价值

二:题型分类:

1:边作为体积的写法:

https://www.acwing.com/problem/content/1076/

void dfs(int u, int fa){
    for(int i = h[u];~i;i = ne[i]){
        int k = v[i];
        if(k == fa) continue;
        dfs(k, u);
        for(int j = m;j >= 0;--j)
            for(int t = 0;t <= j-1;++t)
                dp[u][j] = max(dp[u][j], dp[u][j-t-1] + dp[k][t] + w[i]);
    }
}

2:顶点的数量或者顶点的权值作为体积:

https://www.acwing.com/problem/content/288/

void dfs(int u){
    for(int i = h[u];~i;i = ne[i]){//物品组
        int k = v[i];
        dfs(k);
        for(int j = m-1;j >= 0;--j)//背包容量
            for(int t = 1;t <= j;++t)//决策组内的选择
                dp[u][j] = max(dp[u][j], dp[u][j-t] + dp[k][t]);
    }
    for(int j = m;j >= 0;--j) dp[u][j] = dp[u][j-1] + w[u];
}

https://www.acwing.com/problem/content/10/

void dfs(int u){
    for(int i = h[u];~i;i = ne[i]){
        int k = e[i];
        dfs(k);
        for(int j = m-v[u];j >= 0;--j)
            for(int t = 1;t <= j;++t)
                dp[u][j] = max(dp[u][j], dp[u][j-t] + dp[k][t]);
    }
    //j 从m到0
    for(int j = m;j >= v[u];--j) dp[u][j] = dp[u][j-v[u]] + w[u];
    for(int j = 0;j < v[u];++j) dp[u][j] = 0;
}

题意:给定一颗无向树,带有边权,求边数为q的权值最大值。

分组背包思想:

1.枚举物品组

2.枚举体积

3.枚举决策

依赖关系: i , j , k i,j,k i,j,k依次是父子结点的关系。如果 ( i , j ) (i,j) (i,j)满足要求,则 ( j , k ) (j,k) (j,k)一定满足要求

疑问:为什么 f [ u ] [ j − k − 1 ] f[u][j - k - 1] f[u][jk1] f [ e [ i ] ] [ k ] f[e[i]][k] f[e[i]][k] 不会选到相同的边呢,这是因为这是经过优化过的等式,原式为:

f [ u ] [ i ] [ j ] = m a x ( f [ u ] [ i ] [ j ] , f [ u ] [ i − 1 ] [ j − k − 1 ] + f [ e [ i ] ] [ s [ e [ i ] ] ] [ k ] + w [ i ] ) f[u][i][j] = max(f[u][i][j], f[u][i - 1][j - k - 1] + f[e[i]][s[e[i]]][k] + w[i]) f[u][i][j]=max(f[u][i][j],f[u][i1][jk1]+f[e[i]][s[e[i]]][k]+w[i]) s [ e [ i ] ] s[e[i]] s[e[i]] e [ i ] e[i] e[i]​子树的数量

可以看到 f [ u ] [ i − 1 ] [ j − k − 1 ] f[u][i - 1][j - k - 1] f[u][i1][jk1]只在前 i − 1 i-1 i1棵子树中找,还没到第 i i i​​棵子树,所以两个集合是完全不重合的

通过体积逆向枚举滚动数组优化成二维

f [ i ] [ j ] f[i][j] f[i][j]为从i的子树中选边数为j的最大权值

f[i][j] = max(f[i][j], f[i][k] + f[son][j - k - 1] + w[i,son])

选中当前son为根的 j − k − 1 j-k-1 jk1条边,给其他子树分配 k k k条边,还有一条 i − s o n i-son ison的边

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>

using namespace std;
const int N = 110, M = 220;
int n, q, id, ans;
int head[N], f[N][N];
bool st[N];
struct Edge{
    int to, next, w;
}E[M];

inline void addedge(int u, int v, int w)
{
    E[id].to = v;
    E[id].w = w;
    E[id].next = head[u];
    head[u] = id++;
    return;
}

void dfs(int cur, int father)
{
    for(int i = head[cur]; i != -1; i = E[i].next)
    {
        int p = E[i].to, w = E[i].w;
        if(father == p) continue;
        dfs(p, cur);
        for(int j = q; j; j--) // 循环体积
        {
            for(int k = 0;k < j;k++)	// 循环决策
            {
                f[cur][j] = max(f[cur][j], f[p][k] + f[cur][j - k - 1] + w);
            }
        }
    }
}

int main()
{
    memset(head, -1, sizeof(head));
    memset(st, false, sizeof(st));
    scanf("%d%d", &n, &q);
    for(int i = 1;i < n;i++)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        addedge(a, b, c);
        addedge(b, a, c);
    }
    dfs(1, -1);
    printf("%d\n", f[1][q]);
    return 0;
}

323. 战略游戏 - AcWing题库

树形DP基本题:(士兵可以管到与他相邻所有边,因此一条道路上的两个点必有一个被占领)

f [ i ] [ 0 ] f[i][0] f[i][0]表示当前结点不放士兵

f [ i ] [ 1 ] f[i][1] f[i][1]表示当前节点放士兵

f [ c u r ] [ 0 ] + = f [ s o n ] [ 1 ] f[cur][0] += f[son][1] f[cur][0]+=f[son][1]

f [ c u r ] [ 1 ] + = m i n ( f [ s o n ] [ 0 ] , f [ s o n ] [ 1 ] ) f[cur][1] += min(f[son][0], f[son][1]) f[cur][1]+=min(f[son][0],f[son][1])

从root开始dfs

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
int n, id;
const int N = 1510, M = 3030;
int head[N], f[N][2];
bool isnt_root[N];
struct Edge{
    int to, next;
}E[M];

inline void addedge(int u, int v)
{
    E[id].to = v;
    E[id].next = head[u];
    head[u] = id++;
}

void dfs(int cur)
{
    f[cur][0] = 0, f[cur][1] = 1;
    for(int i = head[cur]; i != -1; i = E[i].next)
    {
        int p = E[i].to;
        dfs(p);
        f[cur][0] += f[p][1];
        f[cur][1] += min(f[p][0], f[p][1]);
    }
}

int main()
{
    while(cin >> n)
    {
        memset(head, -1, sizeof(head));
        memset(isnt_root, false, sizeof(isnt_root));
        id = 0;
        for(int i = 0;i < n;i++)
        {
             int v, num;
             scanf("%d:(%d)", &v, &num);
             for(int j = 0;j < num;j++)
             {
                 int u;
                 scanf("%d", &u);
                 addedge(v, u);
                 isnt_root[u] = true;
             }
        }
        int root = -1;
        for(int i = 0;i <= n - 1;i++)
        {
            if(!isnt_root[i]) root = i;
        }
        dfs(root);
        int ans = min(f[root][0], f[root][1]);
        printf("%d\n", ans);
    }
    return 0;
}

1077. 皇宫看守 - AcWing题库

题意:一个结点可以管到相邻所有结点,问所占结点最少花费

与上一题不同的是,这一题所管理的是点,因此具有三种状态

状态表示:

不放守卫

1.父结点放了,记 f [ u ] [ 0 ] f[u][0] f[u][0]

2.子节点放了,记 f [ u ] [ 1 ] f[u][1] f[u][1]

放置守卫

f [ u ] [ 2 ] f[u][2] f[u][2]其中u为当前的结点

这样就可以涵盖整个状态空间

状态转移:

$f[u][0]=∑min(f[j][1],f[j][2]) $。

含义:当前结点u不放且被父节点看到,那么子结点 j j j只能放或者被其子结点看到。因为 u u u不放所以不能拿 f [ j ] [ 0 ] f[j][0] f[j][0]来更新

f [ u ] [ 1 ] = f [ k ] [ 2 ] + ∑ m i n ( f [ j ] [ 1 ] , f [ j ] [ 2 ] ) f[u][1]=f[k][2]+∑min(f[j][1],f[j][2]) f[u][1]=f[k][2]+min(f[j][1],f[j][2])​。

含义:当前结点u不放,u的子结点k放了,u的其他子结点j放( f [ j ] [ 2 ] f[j][2] f[j][2])或者不放(即 f [ j ] [ 1 ] f[j][1] f[j][1])。没有 f [ j ] [ 0 ] f[j][0] f[j][0]的原因也是因为u不放,且需要枚举k。

f [ u ] [ 2 ] = ∑ m i n ( f [ j ] [ 0 ] , f [ j ] [ 1 ] , f [ j ] [ 2 ] ) f[u][2]=∑min(f[j][0],f[j][1],f[j][2]) f[u][2]=min(f[j][0],f[j][1],f[j][2])​​​。

含义:当前结点放了,那么子结点取哪种状态都可以

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int N = 1510, M = 3030;
bool vis[N];
int head[N], w[N], f[N][3], id, n;
struct Edge{
    int to, next;
}E[M];

inline void addedge(int u, int v)
{
    E[id].to = v;
    E[id].next = head[u];
    head[u] = id++;
    return;
}

void dfs(int cur)
{
    f[cur][2] = w[cur];
    
    int sum = 0;
    for(int i = head[cur]; i != -1; i = E[i].next)
    {
        int p = E[i].to;
        dfs(p);
        f[cur][0] += min(f[p][1], f[p][2]);
        f[cur][2] += min(f[p][0], min(f[p][1], f[p][2]));
        sum += min(f[p][1], f[p][2]);
        //依次假设每个结点放, sum - min(f[p][1], f[p][2])就是其他所有结点的放和不放最小值之和
    }
    
    f[cur][1] = 1e9;
    for(int i = head[cur]; i != -1; i = E[i].next)
    {
        int p = E[i].to;
        f[cur][1] = min(f[cur][1], sum - min(f[p][1],f[p][2]) + f[p][2]);
    }
}

int main()
{
    memset(head, -1, sizeof(head));
    scanf("%d", &n);
    for(int i = 1;i <= n;i++)
    {
        int u, num, idx;
        scanf("%d%d%d", &u, &idx, &num);
        w[u] = idx;
        for(int j = 0; j < num; j++)
        {
            int v;
            scanf("%d", &v);
            vis[v] = true;
            addedge(u, v);
        }
    }
    int root = -1;
    for(int i = 1;i <= n;i++)
    {
        if(!vis[i]) {
            root = i;
            break;
        }
    }
    dfs(root);
    printf("%d\n", min(f[root][1], f[root][2]));
    return 0;
}

root开始dfs

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
int n, id;
const int N = 1510, M = 3030;
int head[N], f[N][2];
bool isnt_root[N];
struct Edge{
    int to, next;
}E[M];

inline void addedge(int u, int v)
{
    E[id].to = v;
    E[id].next = head[u];
    head[u] = id++;
}

void dfs(int cur)
{
    f[cur][0] = 0, f[cur][1] = 1;
    for(int i = head[cur]; i != -1; i = E[i].next)
    {
        int p = E[i].to;
        dfs(p);
        f[cur][0] += f[p][1];
        f[cur][1] += min(f[p][0], f[p][1]);
    }
}

int main()
{
    while(cin >> n)
    {
        memset(head, -1, sizeof(head));
        memset(isnt_root, false, sizeof(isnt_root));
        id = 0;
        for(int i = 0;i < n;i++)
        {
             int v, num;
             scanf("%d:(%d)", &v, &num);
             for(int j = 0;j < num;j++)
             {
                 int u;
                 scanf("%d", &u);
                 addedge(v, u);
                 isnt_root[u] = true;
             }
        }
        int root = -1;
        for(int i = 0;i <= n - 1;i++)
        {
            if(!isnt_root[i]) root = i;
        }
        dfs(root);
        int ans = min(f[root][0], f[root][1]);
        printf("%d\n", ans);
    }
    return 0;
}

1077. 皇宫看守 - AcWing题库

题意:一个结点可以管到相邻所有结点,问所占结点最少花费

与上一题不同的是,这一题所管理的是点,因此具有三种状态

状态表示:

不放守卫

1.父结点放了,记 f [ u ] [ 0 ] f[u][0] f[u][0]

2.子节点放了,记 f [ u ] [ 1 ] f[u][1] f[u][1]

放置守卫

f [ u ] [ 2 ] f[u][2] f[u][2]其中u为当前的结点

这样就可以涵盖整个状态空间

状态转移:

$f[u][0]=∑min(f[j][1],f[j][2]) $。

含义:当前结点u不放且被父节点看到,那么子结点 j j j只能放或者被其子结点看到。因为 u u u不放所以不能拿 f [ j ] [ 0 ] f[j][0] f[j][0]来更新

f [ u ] [ 1 ] = f [ k ] [ 2 ] + ∑ m i n ( f [ j ] [ 1 ] , f [ j ] [ 2 ] ) f[u][1]=f[k][2]+∑min(f[j][1],f[j][2]) f[u][1]=f[k][2]+min(f[j][1],f[j][2])​。

含义:当前结点u不放,u的子结点k放了,u的其他子结点j放( f [ j ] [ 2 ] f[j][2] f[j][2])或者不放(即 f [ j ] [ 1 ] f[j][1] f[j][1])。没有 f [ j ] [ 0 ] f[j][0] f[j][0]的原因也是因为u不放,且需要枚举k。

f [ u ] [ 2 ] = ∑ m i n ( f [ j ] [ 0 ] , f [ j ] [ 1 ] , f [ j ] [ 2 ] ) f[u][2]=∑min(f[j][0],f[j][1],f[j][2]) f[u][2]=min(f[j][0],f[j][1],f[j][2])​​​。

含义:当前结点放了,那么子结点取哪种状态都可以

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int N = 1510, M = 3030;
bool vis[N];
int head[N], w[N], f[N][3], id, n;
struct Edge{
    int to, next;
}E[M];

inline void addedge(int u, int v)
{
    E[id].to = v;
    E[id].next = head[u];
    head[u] = id++;
    return;
}

void dfs(int cur)
{
    f[cur][2] = w[cur];
    
    int sum = 0;
    for(int i = head[cur]; i != -1; i = E[i].next)
    {
        int p = E[i].to;
        dfs(p);
        f[cur][0] += min(f[p][1], f[p][2]);
        f[cur][2] += min(f[p][0], min(f[p][1], f[p][2]));
        sum += min(f[p][1], f[p][2]);
        //依次假设每个结点放, sum - min(f[p][1], f[p][2])就是其他所有结点的放和不放最小值之和
    }
    
    f[cur][1] = 1e9;
    for(int i = head[cur]; i != -1; i = E[i].next)
    {
        int p = E[i].to;
        f[cur][1] = min(f[cur][1], sum - min(f[p][1],f[p][2]) + f[p][2]);
    }
}

int main()
{
    memset(head, -1, sizeof(head));
    scanf("%d", &n);
    for(int i = 1;i <= n;i++)
    {
        int u, num, idx;
        scanf("%d%d%d", &u, &idx, &num);
        w[u] = idx;
        for(int j = 0; j < num; j++)
        {
            int v;
            scanf("%d", &v);
            vis[v] = true;
            addedge(u, v);
        }
    }
    int root = -1;
    for(int i = 1;i <= n;i++)
    {
        if(!vis[i]) {
            root = i;
            break;
        }
    }
    dfs(root);
    printf("%d\n", min(f[root][1], f[root][2]));
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值