2022暑假 动态规划总结(进阶)


一、区间dp

区间dp基本思路:
三层for循环,先枚举区间长度,再枚举区间左端点,最后再枚举区间内部

1. 石子合并

题目描述:
设有 N 堆石子排成一排,其编号为 1,2,3,…,N。每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N 堆石子合并成为一堆。每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。
例如有 4 堆石子分别为 1 3 5 2, 我们可以先合并 1、2 堆,代价为 4,得到 4 5 2, 又合并 1,2 堆,代价为 9,得到 9 2 ,再合并得到 11,总代价为 4+9+11=24;如果第二步是先合并 2,3 堆,则代价为 7,得到 4 7,最后一次合并代价为 11,总代价为 4+7+11=22。
问题是:找出一种合理的方法,使总的代价最小,输出最小代价。
测试样例:

输入:
4
1 3 5 2
输出:
22
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 310;
int n;
int s[N];
int f[N][N];//表示区间i到j的最小代价

int main()
{
    scanf("%d", &n);
    for(int i = 1; i <= n; i ++ )
    {
        scanf("%d", &s[i]);
    }
    for(int i = 1; i <= n; i ++ )
    {
        s[i] += s[i - 1];   //前缀和
    }
    for(int len = 2; len <= n; len ++ )//枚举步长
    {
        for(int i = 1; i + len - 1 <= n; i ++ )//枚举左端点
        {
            int l = i, r = i + len - 1;
            f[l][r] = 1e8;
            for(int k = l; k < r; k ++ )//枚举当前区间,并在这个区间内找最小值
            {

                f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);//这里有点像floyd
            }
        }
    }
    printf("%d\n", f[1][n]);
    return 0;
}

2. 石子合并

#include<iostream>
using namespace std;

int n,t;
int cost[110];
int f[110][110];

int main()
{
    cin>>t;
    while(t--)
    {
        cin>>n;
        for(int i=0; i<n; i++) scanf("%d",&cost[i]);
        for(int step=2; step<=n; step++)
        {
            for(int i=0; i<=n-step; i++)
            {
                int l=i,r=i+step-1;
                f[l][r]=1e9;
                for(int j=l; j<=r; j++)
                {
                    if(j==l) f[l][r]=min(f[l][r],f[j+1][r]+cost[j]*(r-l));
                    else if(j==r) f[l][r]=min(f[l][r],f[l][j-1]+cost[j]*(r-l));
                    else f[l][r]=min(f[l][r],f[l][j-1]+f[j+1][r]+cost[j]*(r-l));
                }
            }
        }
        cout<<f[0][n-1]<<endl;
    }
    return 0;
}

3. 括号配对(牛客)

在这里插入图片描述

输入:
[])
输出:
1
#include<iostream>
#include<cstring>
#define inf 0x3f3f3f3f
using namespace std;

int f[110][110];
char s[110];

bool check(char a,char b)
{
    if(a=='(' && b==')') return true;
    if(a=='[' && b==']') return true;
    return false;
}

int main()
{
    scanf("%s",s+1);
    int n=strlen(s+1);
    
    //从外向里缩小到单独一个
    for(int i=1;i<=n;i++) f[i][i]=1;
    
    for(int len=2;len<=n;len++)
    {
        for(int i=1;i+len-1<=n;i++)
        {
            int j=i+len-1;//右区间最大到n
            f[i][j]=inf;//因为区间dp是从小往大枚举,当前长度的区间是没有枚举过的,而比他小的所有区间都被枚举过,所以,如果找最小值的话,要先把它初始化为无穷大
            for(int k=i;k<=j;k++)
            {
                f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]);
            }
            //当i与j相邻时,此时如果取后者,会出现左大于右的情况,而它的值为0,符合实际情况
            if(check(s[i],s[j])) f[i][j]=min(f[i][j],f[i+1][j-1]);
        }
    }
    cout<<f[1][n]<<endl;
    return 0;
}

4. 凸多边形的划分(牛客)

在这里插入图片描述

输入:
5
121 122 123 245 231
输出:
12214884
n=eval(input())
a=list(map(int,input().split()))
a+=a # 以每个点作为交点,都遍历一遍
a=[0]+a
f=[[1e40 for i in range(110)] for j in range(110)]

for i in range(1,2*n):
    f[i][i+1]=0 #注意初始化,两个的时候构不成三角形
for len in range(3,n+1):
    for i in range(1,2*n-len+1): #i+len-1<=2*n-1
        j=i+len-1
        for k in range(i+1,j):
            f[i][j]=min(f[i][j],f[i][k]+f[k][j]+a[i]*a[k]*a[j]) #这里比较抽象
ans=f[1][n]
for i in range(1,n+1):
    ans=max(ans,f[i][i+n-1])
print(ans)

二、数位dp

1. 计数问题

在这里插入图片描述

在这里插入图片描述

上述讨论中,有两种情况需要特判:

  1. 当要找的数在最高位时,(1)情况不存在,所以忽略不计。
  2. 当要找的数为0时,首先不能在最高位上找0,其次,xx…x要从00…1开始,因为如果从00…0开始找的话,会出现前导零。另外,不是从100…0开始找,这种是一个错误的想法,因为00…1不会出现重复的情况。
输入:
1 10
44 497
346 542
1199 1748
1496 1403
1004 503
1714 190
1317 854
1976 494
1001 1960
0 0
输出:
1 2 1 1 1 1 1 1 1 1
85 185 185 185 190 96 96 96 95 93
40 40 40 93 136 82 40 40 40 40
115 666 215 215 214 205 205 154 105 106
16 113 19 20 114 20 20 19 19 16
107 105 100 101 101 197 200 200 200 200
413 1133 503 503 503 502 502 417 402 412
196 512 186 104 87 93 97 97 142 196
398 1375 398 398 405 499 499 495 488 471
294 1256 296 296 296 296 287 286 286 247
#include<iostream>
#include<vector>
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=res*10;
    return res;
}

int f(int n,int x)
{
    vector<int> num;
    while(n)
    {
        num.push_back(n%10);
        n=n/10;
    }
    n=num.size();
    int res=0;
    for(int i=n-1-!x;i>=0;i--) //这里枚举的出现在每一位上的x
    {
        if(i<n-1)//当枚举最高位时,这里跳过
        {
            res+=get(num,n-1,i+1)*power10(i);
            if(!x) res-=power10(i);//找0时要从000..1开始
        }
        if(num[i]==x) res+=get(num,i-1,0)+1;
        else if(num[i]>x) res+=power10(i);
    }
    return res;
}

int main()
{
    int a,b;
    while(cin>>a>>b,a&&b)
    {
        if(a>b) swap(a,b);
        for(int i=0;i<=9;i++)
        {
            cout<<f(b,i)-f(a-1,i)<<" ";
        }
        cout<<endl;
    }
    return 0;
}

三、树形dp

1. 没有上司的舞会

2. 左孩子右兄弟

#include <iostream>
#include <cstring>

using namespace std;

const int N  = 1e5 + 10;

int h[N], ne[N], e[N], idx;

void add(int a, int b)
{
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx++;
}

int find(int x)
{
    int cnt = 0, m = -1;
    for(int i = h[x]; i != -1; i = ne[i])
    {
        cnt++;
        int k = e[i];
        m = max(m, find(k));  
    }

    if(cnt == 0) return 1;
    return m + cnt;   
}

int main()
{
    int n ;
    cin >> n;

    memset(h, -1, sizeof h);

    for(int i = 2;  i <= n; i++)
    {
        int x;
        cin >> x;
        add(x, i);//x指向i
    }

    cout << find(1) - 1;

    return 0;
}

四、状压dp

1. 蒙德里安的梦想

在这里插入图片描述
在这里插入图片描述

输入:
1 2
1 3
1 4
2 2
2 3
2 4
2 11
4 11
0 0

输出:
1
0
1
2
3
5
144
51205
#include<bits/stdc++.h>
using namespace std;

const int N=12, M = 1<< N;

long long f[N][M] ;// 第一维表示列, 第二维表示该列某个具体的状态

bool st[M];  //存储每种状态是否有奇数个连续的0,如果奇数个0是无效状态,如果是偶数个零置为true。

//vector<int > state[M];  //二维数组记录合法的状态
vector<vector<int>> state(M);  //两种写法等价:二维数组
int m, n;

int main()
{

    while(cin>>n>>m, n||m)  //读入n和m,并且不是两个0即合法输入就继续读入
    {

        //第一部分:预处理1
        //对于每种状态,先预处理每列不能有奇数个连续的0

        for(int i=0; i< 1<<n; i++)
        {
            int cnt =0 ;//记录连续的0的个数
            bool isValid = true; // 某种状态没有奇数个连续的0则标记为true
            for(int j=0; j<n; j++) //遍历这一列,从上到下
            {
                if( i>>j &1)   //i>>j位运算,表示i(i在此处是一种状态)的二进制数的第j位; &1为判断该位是否为1,如果为1进入if
                {
                    if(cnt &1)   //这一位为1,看前面连续的0的个数,如果是奇数(cnt &1为真)则该状态不合法
                    {
                        isValid =false;
                        break;
                    }
                    cnt=0; // 既然该位是1,并且前面不是奇数个0(经过上面的if判断),计数器清零。//其实清不清零没有影响
                }
                else cnt++; //否则的话该位还是0,则统计连续0的计数器++。
            }
            if(cnt &1)  isValid =false; //最下面的那一段判断一下连续的0的个数
            st[i]  = isValid; //状态i是否有奇数个连续的0的情况,输入到数组st中
        }

        //第二部分:预处理2
        // 经过上面每种状态 连续0的判断,已经筛掉一些状态。
        //下面来看进一步的判断:看第i-2列伸出来的和第i-1列伸出去的是否冲突

        for(int j=0; j< 1<<n; j++) //对于第i列的所有状态
        {
            state[j].clear(); //清空上次操作遗留的状态,防止影响本次状态。
            for(int k=0; k< 1<<n; k++) //对于第i-1列所有状态
            {
                if((j&k )==0 && st[ j| k] ) // 第i-2列伸出来的 和第i-1列伸出来的不冲突(不在同一行)
                    //解释一下st[j | k]
                    //已经知道st[]数组表示的是这一列没有连续奇数个0的情况,
                    //我们要考虑的是第i-1列(第i-1列是这里的主体)中从第i-2列横插过来的,还要考虑自己这一列(i-1列)横插到第i列的
                    //比如 第i-2列插过来的是k=10101,第i-1列插出去到第i列的是 j =01000,
                    //那么合在第i-1列,到底有多少个1呢?自然想到的就是这两个操作共同的结果:两个状态或。 j | k = 01000 | 10101 = 11101
                    //这个 j|k 就是当前 第i-1列的到底有几个1,即哪几行是横着放格子的

                    state[j].push_back(k);  //二维数组state[j]表示第j行,
                //j表示 第i列“真正”可行的状态,如果第i-1列的状态k和j不冲突则压入state数组中的第j行。
                //“真正”可行是指:既没有前后两列伸进伸出的冲突;又没有连续奇数个0。
            }
        }

        //第三部分:dp开始

        memset(f,0,sizeof f);  //全部初始化为0,因为是连续读入,这里是一个清空操作。类似上面的state[j].clear()

        f[0][0]=1 ;// 这里需要回忆状态表示的定义,按定义这里是:前第-1列都摆好,且从-1列到第0列伸出来的状态为0的方案数。
        //首先,这里没有-1列,最少也是0列。其次,没有伸出来,即没有横着摆的。即这里第0列只有竖着摆这1种状态。

        for(int i=1; i<= m; i++) //遍历每一列:第i列合法范围是(0~m-1列)
        {
            for(int j=0; j< 1<<n; j++)   //遍历当前列(第i列)所有状态j
            {
                for( auto k : state[j])    // 遍历第i-1列的状态k,如果“真正”可行,就转移
                    f[i][j] += f[i-1][k];    // 当前列的方案数就等于之前的第i-1列所有状态k的累加。
            }
        }

        //最后答案是什么呢?
        //f[m][0]表示 前m-1列都处理完,并且第m-1列没有伸出来的所有方案数。
        //即整个棋盘处理完的方案数

        cout<< f[m][0]<<endl;
    }
}

2. 积木画

在这里插入图片描述

输入:
3

输出:
5
#include<iostream>
using namespace std;

const int mod=1e9+7,N=1e7+10;
int n;
int f[N][4];//第一维表示画布的每一列,第二维表示每一列的某个状态,2^2

int g[4][4]={
    {1,1,1,1},
    {0,0,1,1},
    {0,1,0,1},
    {1,0,0,0}
};//状态转移

int main()
{
    cin>>n;
    f[1][0]=1;
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<4;j++)//前
        {
            for(int k=0;k<4;k++)//后
            {
                if(g[j][k])
                {
                    f[i+1][k]=(f[i+1][k]+f[i][j])%mod;
                }
            }
        }
    }//可以使用滚动数组
    cout<<f[n+1][0]<<endl;
    return 0;
}

3. 最短Hamilton路径

在这里插入图片描述

输入:
5
0 2 4 5 1
2 0 6 5 3
4 6 0 8 3
5 5 8 0 5
1 3 3 5 0

输出:
18
#include<iostream>
#include<cstring>
using namespace std;

const int N=20,M=1<<N;
int f[M][N];//走到j点,方案为i的最短距离
int w[N][N];
int n;

int main()
{
    cin>>n;
    for(int i=0;i<n;i++)
    {
        for(int j=0;j<n;j++)
        {
            cin>>w[i][j];
        }
    }
    memset(f,0x3f,sizeof f);
    f[1][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)
                    {
                        f[i][j]=min(f[i][j],f[i-(1<<j)][k]+w[k][j]);
                    }
                }
            }
        }
    }
    cout<<f[(1<<n)-1][n-1]<<endl;
    return 0;
}

五、记忆化搜索

1. 滑雪

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

输出格式
输出一个整数,表示可完成的最长滑雪长度。

数据范围
1≤R,C≤300,
0≤矩阵中整数≤10000
输入样例:

5 5
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9

输出样例:

25
#include<iostream>
#include<cstring>
using namespace std;

const int N=310;
int n,m;
int g[N][N],f[N][N];
int dx[4]={0,-1,0,1};
int dy[4]={1,0,-1,0};

int dp(int x,int y)
{
    if(f[x][y]!=-1) return f[x][y];//这一行非常关键,减少重复搜索
    f[x][y]=1;
    for(int i=0;i<4;i++)
    {
        int a=x+dx[i],b=y+dy[i];
        if(a>=0&&a<n&&b>=0&&b<m&&g[x][y]>g[a][b])
            f[x][y]=max(f[x][y],dp(a,b)+1);//当开始统计最大值的时候,一定是递归到尽头,开始逐层返回,返回当前位置和下一个位置路径长度的最大值
    }
    return f[x][y];
}

int main()
{
    int res=0;
    cin>>n>>m;
    for(int i=0;i<n;i++)
        for(int j=0;j<m;j++)
            cin>>g[i][j];
    memset(f,-1,sizeof f);
    for(int i=0;i<n;i++)
        for(int j=0;j<m;j++)
            res=max(res,dp(i,j));
    cout<<res<<endl;
    return 0;
}

2. 石子合并

#include<iostream>
#include<cstring>
using namespace std;

int f[1010][1010];
int n;
int s[1010];

int dp(int l,int r)
{
    if(l==r) return 0;
    if(f[l][r]!=-1) return f[l][r];
    f[l][r]=1e9;
    for(int i=l;i<=r-1;i++)
    {
        f[l][r]=min(dp(l,r),dp(l,i)+dp(i+1,r)+s[r]-s[l-1]);//注意边界问题
    }
    return f[l][r];
}

int main()
{
    cin>>n;
    memset(f,-1,sizeof f);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&s[i]);
        s[i]=s[i]+s[i-1];
    }
    cout<<dp(1,n)<<endl;
    return 0;
}

3. 分治

在这里插入图片描述

输入:
2
1
1
3
1 1 2
输出:
0
2
#include<iostream>
#include<cstring>
using namespace std;

int f[110][110];
int t,n;
int cost[110];

int dp(int l,int r)
{
    if(l>=r) return 0;//注意这里,一定是>=
    if(f[l][r]!=-1) return f[l][r];
    f[l][r]=1e9;
    for(int i=l;i<=r;i++)
    {
        f[l][r]=min(dp(l,r),dp(l,i-1)+dp(i+1,r)+cost[i]*(r-l));
    }
    return f[l][r];
}

int main()
{
    cin>>t;
    while(t--)
    {
        cin>>n;
        for(int i=0;i<n;i++) scanf("%d",&cost[i]);
        memset(f,-1,sizeof f);
        cout<<dp(0,n-1)<<endl;
    }
    return 0;
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

勇敢nn

心动不如行动,感谢您的支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值