动态规划-算法题-整理

https://mmbiz.qpic.cn/mmbiz_png/cXoa07I9qrkUjIictMgFqhqd75N6WHCzCoOzibOiazNsg4e5jBsZGC3vZUL7zx29x1zfFVuCgKRib28SllE4pxxUyw/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1

部分转载自 “小浩算法”微信公粽号!真心良心的公众号!

目录

1、数学模拟

1、最大连续子序和

2、最长上升子序列

3、三角形最小路径和(二叉树)

4、整数拆分任意次,乘积最大

5、整数拆分k次,乘积最大

6、网格最小路径

7、打家劫舍-链表,挨着不能同时偷

8、打家劫舍-环,挨着不能同时偷

8、打家劫舍-树,挨着的层不能同时偷

2、银行等问题

3、特殊的dp

壮压dp

方格取数(1)

胜利大逃亡(续)-壮压+bfs

2、插头dp


1、数学模拟


1、最大连续子序和

dp[i]是以i结尾的自序列里和最大的和。因为题目要求是连续自序列,所以dp[i]只与dp[i-1]有关。所以就只有两种情况,包含a[i-1]和不包含a[i-1]。因此dp[i]=max(dp[i-1]+a[i],a[i])

oid dp_1(){
    int a[11]= {-2,1,-3,4,0,2,0,-5,4,-7,6};
    vector<int> aa(a,a+ sizeof(a)/ sizeof(int));
    vector<int> yy(aa.size());
    yy[0]=aa[0];
    for(int i=1;i<aa.size();i++)
        yy[i]=max(yy[i-1]+aa[i],aa[i]);
    cout<<*max_element(yy.begin(),yy.end())<<endl;
    int end=distance(yy.begin(),max_element(yy.begin(),yy.end()));//从左往右,的最大值。如果最大值有几个?
    int start,sum=yy[end];
    for(int i=end;i>=0;i--){
        if(sum-aa[i])  sum-=aa[i];
        else {start=i;  break;}
    }
    for(int i=start;i<=end;i++)
        cout<<aa[i]<<" ";
}

2、最长上升子序列

方法1: 这里面的序列可以不连续,因此 dp[i] 与dp[0 ~ i-1]有关。因此dp[i]=max(dp[n]+1,1) if a[n]<=a[i] for n in range(0,i-1)

方法2: 其实可以维护一个序列,这个序列的长度就是最终的最长上升子序列长度,但是内部不是这个最长上升子序列。这个序列存的是角标,角标对应的值也是一个递增序列。当前的值往这个递增序列里加,如果比最大的还大,那么直接放后面,如果不是,就找里面第一个比他大的数,替换为当前的数!替换!意味着这个子序列不变,但是把之前大的换为小的,有利于后面序列增加!因为前缀树更小了!而替换又不改变真实的上升子序列的长度。只有大于最大值才改变长度,所以ok。

而找第一个比他大的数这里,因为序列一定是上升序列,所以可以用二分!

//最长上升子序列
void dp_2(){
    int a[8]={10,9,2,5,3,7,101,18};
    //方法1:
    vector<int> seq(8,1);
    for(int i=1;i<seq.size();i++)
    {
        if(a[i]>=a[i-1]) seq[i]=seq[i-1]+1;
        else {
            for (int j = 0; j < i; j++)
                if(a[i]>=a[j]) seq[i]=max(seq[i],seq[j]+1);
        }
    }
    cout<<*max_element(seq.begin(),seq.end())<<endl;
    //方法2:
    vector<int> dp(sizeof(a)/sizeof(int),1);
    vector<int> a_seq; a_seq.push_back(0);//a_seq里存的是下标
    for(int i=1;i<dp.size();i++)
    {
        if(a[i]>=a[a_seq[a_seq.size()-1]]) {a_seq.push_back(i);dp[i]=a_seq.size();}
        else{
            int l=0,r=a_seq.size()-1;
            while (l<=r){//找当前seq里最右边那个<=a[i]的位置。
                int min=l+(r-l)/2;
                if(a[min]<=a[i]) l=min+1;
                else r=min-1;
            }
            a_seq[r+1]=i;//把seq里第一个比a[i]大的数的位置替换为a[i]饿的位置
            dp[i]=r+1;
        }
    }
    cout<<a_seq.size()<<endl;
}

3、三角形最小路径和(二叉树)

题目:也就是说,给定一个二维数组,他只能从左上角,每次右下/左下走。问走到最后一行的最小路径。

方法:首先题目虽然是三角形,但是可以按照正常数组对齐之后来看的话,其实就是只能往下/右下走。因此可以把dp[i]看作当前层走第i个的情况下,最小路径。dp[i]=min(dp_pre[i]+a[i],dp_pre[i-1]+a[i])注意要用到dp_pre[i-1],所以要从右往左遍历.

//三角形最小路径和
void dp_3(){
    int a[4][4]={{2},{3,4},{6,5,7},{4,1,8,3}};
    vector<vector<int> > triangle(4,vector<int>(4));//二维数组,只能一个一个赋值
    for(int i=0;i<4;i++)
        for(int j=0;j<=i;j++)
            triangle[i][j]=a[i][j];
    if(triangle.size()==0) {cout<<0; return;}
    if(triangle.size()==1) {cout<<*min_element(triangle[0].begin(),triangle[0].end());  return;}
    vector<int> p;
    p.push_back(triangle[0][0]);
    for(int i=1;i<triangle.size();i++)
    {
        p.push_back(0);
        for(int j=triangle[i].size()-1;j>=0;j--)
        {
            if(j==p.size()-1) p[j]=p[j-1]+triangle[i][j];
            else if(j==0) p[j]=p[j]+triangle[i][j];
            else p[j]=min(p[j]+triangle[i][j],p[j-1]+triangle[i][j]);
        }
    }
    cout<<*min_element(p.begin(),p.end());
}

4、整数拆分任意次,乘积最大

方法1:凑数,任意拆分的话,那么就是尽可能拆3,3不行就尽可能拆为2。拆为3,最多能拆n/3个,但是最后剩余的应该是1或2。如果是2,之后就ok,但是如果是1,就要把最后一个3,变为3+1=2+2。但是3以内要特殊考虑是n-1。
方法2: dp[i]:i拆分任意份,最大的乘积。 dp[i]=max(dp[i],max(dp[j],j)*(i-j))注意!dp[j]可能<j,因为k>=2,一定要分!,但是如果j以内不是最终结果,而是部分,可以为j。因为i拆分任意份,最大的乘积在计算中是max(dp[j],j)。
//整数拆分:n分任意大于等于2份,乘积最大
void dp_5(){
    int n;cin>>n;
    //方法1:凑数,任意拆分的话,那么就是尽可能拆3,3不行就尽可能拆为2。拆为3,最多能拆n/3个,但是最后剩余的应该是1或2。如果是2,之后就ok,但是如果是1,就要把最后一个3,变为3+1=2+2
    if(n<=3) cout<<n-1<<endl; //!!!注意!!!3以内
    else {
        int num = n / 3;
        int num1 = n % 3;
        if (num1 == 2) cout << pow(3, num) * 2<<endl;
        else if (num1 == 1) cout << pow(3, num - 1) * 4<<endl;
        else cout << pow(3, num)<<endl;
    }
    //方法2:dp
    vector<int> dp(n+1,0);//dp[i]i拆分任意份,最大的乘积。
    dp[1]=1;
    //for(int i=0;i<=n;i++) dp[i]=i;//注意!分为>=2份,意味着不能是本身!所以乘积最大值可能<n,比如n=2,
    for(int i=1;i<=n;i++)
        for(int j=1;j<i;j++)
            dp[i]=max(dp[i],max(dp[j],j)*(i-j));
    cout<<dp[n]<<endl;
}

 

5、整数拆分k次,乘积最大

方法1: 也就是已知k个数的和,求乘积最大,那么一些数更倾向于平均的时候乘积最大。而平均的话,那就希望是n/k,k个数,那么就平均了,但是很可能n/k不能整数,那么!其实就是希望这k个数只有两种可能就是n/k和n/k+1,这样就平均了,那么就要求这两个数分别家几个,n%k可以看作n/k之后还剩几,那么可以看作,n整除k之后多了几个1,也就是n/k+1有几个。k-n%k就是n/k有几个

方法2: dp的方法,其实n拆为k个数,可以看作i拆为k-1个数和j。因此dp[i][kk]可以看作:和为i,切kk次,最大的乘积=max(dp[i][kk],dp[i-j][kk-1]*j)

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

int main() {
    int k,n;cin>>k>>n;
    //方法1:一些数和固定,乘积要最大,其实就是这些数越平均,乘积越大!所以希望分为的数只有(n/k)或(n/k+1),而我们要求的就是这两个数分别多少个
    long long a=n/k;
    long long b=n%k;//相当于n/k整除,之后还剩几个1!!,剩几个1,就代表(n/k+1)有几个。
    long long bb=b;
    long long ans=1;
    while(bb--) ans*=(a+1);
    bb=k-b;
    while (bb--)   ans*=a;
    cout<<ans<<endl;
    //方法2:
    vector<vector<int> > dp(n+1,vector<int>(k+1,1));
    //dp[i][kk]:和为i,切kk次,最大的乘积=max(dp[i][kk],dp[i-j][kk-1]*j)
    for(int i=1;i<=n;i++)
        for(int kk=1;kk<=k;kk++)
            for(int j=1;j<=i;j++)
                dp[i][kk]=max(dp[i][kk],dp[i-j][kk-1]*j);
    cout<<dp[n][k]<<endl;
    return 0;
}

6、网格最小路径

给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

当前到i,j点的最小路径dp[i][j]=min(左边的dp[i-1][j],上边的dp[i][j-1])+input[i-1][j-1]

void dp_6(){
    freopen("../a.txt","r",stdin);
    int num;cin>>num;
    vector<vector<int> > input(num,vector<int>(num));
    vector<vector<int> > dp(num+1,vector<int>(num+1,0));
    for(int i=0;i<num;i++)
        for(int j=0;j<num;j++)
            cin>>input[i][j];
    //dp[1][1]=input[0][0];
    for(int i=1;i<=num;i++)
        for(int j=1;j<=num;j++)
        {
            if(i==1&&j==1) dp[i][j]=input[i-1][j-1];
            else if(i==1) dp[i][j]=dp[i][j-1]+input[i-1][j-1];
            else if(j==1) dp[i][j]=dp[i-1][j]+input[i-1][j-1];
            else dp[i][j]=min(dp[i-1][j],dp[i][j-1])+input[i-1][j-1];
           // cout<<i<<" "<<j<<" "<<dp[i][j]<<endl;
        }
    cout<<dp[num][num]<<endl;
}

7、打家劫舍-链表,挨着不能同时偷

leetcode 198 也就是当前的dp[i],看 上个/上上个+当前 这两个找最大值

int dp_7(){
    vector<int> nums;
    int n;
    while(cin>>n) nums.push_back(n);
    if(nums.size()==0) return 0;
    vector<int> dp(nums.size());

   for(int i=0;i<nums.size();i++){
       if(i==0) dp[i]=nums[i];
       else if(i==1) dp[i]=max(nums[0],nums[1]);
       else dp[i]=max(dp[i-1],dp[i-2]+nums[i]);//其实now只和pre和now有关
   }
   cout<<dp[nums.size()-1];
    return 0;
}

8、打家劫舍-环,挨着不能同时偷

leetcode 213 环其实就是说明第一个0和最后一个n-1不能同时偷,那么其实就相当于 max(偷0-n-2/偷1-n-1).调用两边上一题的代码,上一题可以优化,因为只与上一个和上上个有关,所以只需要保存pre/now
int dp_8_7(vector<int> nums,int s,int e){
    int pre=nums[s],now=max(nums[s],nums[s+1]);
      for(int i=s+2;i<=e;i++)
      {   int temp=now;
          now=max(now,pre+nums[i]);
          pre=temp;
      }
    return now;
}

int dp_8(){
    vector<int> nums;
    int n;
    while(cin>>n) nums.push_back(n);
    if(nums.size()==0) return 0;
    if(nums.size()==1) return nums[0];
    if(nums.size()==2) return max(nums[0],nums[1]);
    if(nums.size()==3) return max(nums[0],max(nums[1],nums[2]));
    else return max(dp_8_7(nums,0,nums.size()-2),dp_8_7(nums,1,nums.size()-1));
    return 0;
}

8、打家劫舍-树,挨着的层不能同时偷

leetcode 337 二叉树,一定要隔层偷。
相同的思想,记录下一层的最大和/下下层的最大和。但是下面层分不同的支路。res(2)表示当前下一层和对应的下下层的和。
因此return res-> res[0](上一层的下层,所以是用了当前root的,那么就一定用隔层的)= resl[1]+resr[1]+val
res[1]=(没用当前这个,可能用的下/下下层中大的那个)=max(resl[0],resl[1])+max(resr[0],resr[1])
class Solution:
    def hh1(self,root):
        #返回的值是用当前这一层的最大值和使用当前这一层的下一层的最大值
        res = [0, 0]
        if root==None: return res
        res1=self.hh1(root.left)
        res2=self.hh1(root.right)
        #不使用当前点的最大值,其实是!!4种情况1/2 -》1子节点/2的子节点一共4个点,父子节点不能同时使用,所以,其实就是分别找最大值加起来就是不使用当前根结点的最大值!
        res[1]=max(res2[0],res2[1])+max(res1[0],res1[1])
        res[0]=res1[1]+res2[1]+root.val
        return res

    def rob(self, root: TreeNode) -> int:
        #其实还是或者是要跟+跟的孙子/只要跟的儿子,所以其实留2层就ok了
        if root==None: return 0
        return max(self.hh1(root))

2、银行等问题

1、堆金币问题

https://www.nowcoder.com/questionTerminal/6d3ccbc5b6ad4f12b8fe4c97eaf969e0?orderByHotValue=1&page=1&onlyReference=false

有 N 堆金币排成一排,第 i 堆中有 C[i] 块金币。每次合并都会将相邻的两堆金币合并为一堆,成本为这两堆金币块数之和。经过N-1次合并,最终将所有金币合并为一堆。请找出将金币合并为一堆的最低成本。

其中,1 <= N <= 30,1 <= C[i] <= 100

因为说了只能相邻的合并,因此可以看成dp[i][j]:从0号金币到j号金币合为一堆的最低成本。

每次合并都是两个两个合并,因此可以先看,两个合并的最低成本,[0,1] [1,2]这样,

之后看三个合并的最低成本,其实就是[0,1]+2 / 0+[1,2]

看[i][i+len]这个区间和为一个堆,要花费多少,那么其实可以分为2个区间,[i][j]+[j][i+len]+这次就是sum[i,i+len]

#include<iostream>
#include<vector>
#include <numeric>
using namespace std;
 
int vec_sum(vector<int> h,int b,int e){
    int ans=0;
    for(int i=b;i<=e;i++)
        ans+=h[i];
    return ans;
}
 
int main(){
    int num,ans=0,len=1;
    cin>>num;
    vector<int> jinbi(num,0);
    vector<vector<int> > he;
    for(int i=0;i<num;i++)
    {
        vector<int> jinbii(num,0x7ffffff);
        he.push_back(jinbii);
    }
    for(int i=0;i<num;i++)
    {
        cin>>jinbi[i];
        he[i][i]=0;
    }
    while(len<num){
        for(int i=0;i+len<num;i++)
        {
            for(int j=i;j+1<=i+len;j++) //看[i][i+len]这个区间和为一个堆,要话费多少,那么其实可以分为2个区间,[i][j]+[j][i+len]+这次就是sum[i,i+len]
                he[i][i+len]=min(he[i][i+len],he[i][j]+he[j+1][i+len]+vec_sum(jinbi,i,j)+vec_sum(jinbi,j+1,i+len));
        }
        len++;
    }
    cout<<he[0][num-1];
}

3、特殊的dp

壮压dp

方格取数(1)


 http://acm.hdu.edu.cn/showproblem.php?pid=1565

/* 思路:状压DP
题目中 n<=20,但仅需枚举每一行中满足条件的状态,当实际上当 n=20 时,合法状态为 16,因此实际上 n<=16
当前[i][j] 当前第i行取第j种状态。这个状态其实对于每一行来讲,是固定个数的,因为每一行其实也有的状态是不行的,1110就不行。1010101这样才行。但是如果真取了,还要看上一行
sum[i][j] 就是第i行,取第j种状态,第i 行的和。
sta[len] 只看一行的状态,其实每一行是一样的,也就是横向不冲突的话,有len种。之后 sta[k]记录的是当前这种状态是那种状态。
而什么样的状态横向不冲突,其实就是 状态i (i&(i>>1))=0的时候意味着i他边不相邻。
而当前行sta[k1] 和上一行sta[k2] 怎么看不冲突,sta[k1]&sta[k2]=0就代表不冲突
用 dp[i][j] 表示前 i 行的第 i 行取 j 种状态时的最大和,用 sum[i][j] 表示第 i 行取第 j 个状态的取值总和,则有 dp[i][j] = max(dp[i][j], dp[i-1][k] + sum[i][j]) ,当两行状态相与为 0 即可进行状态转移
方法2:
/*也可以用网络流/求二分图的最大独立问题。可以看作一个图上选一些点,满足点与点之间都不直接连接,也就是没有边。
* https://blog.csdn.net/u011742541/article/details/16908975

#define INF 0x3f3f3f3f
#define PI acos(-1.0)
#define N 16
#define MOD 10007
#define E 1e-6
#define LL long long

int nn;
int a[21][21];
int dp[21][1<<N],sum[21][1<<N];
int sta[1<<N];
int calculate(int k,int state){//求第i行状态为state时的取值总和
    int res=0;
    for(int i=0;i<nn;i++)
        if((state>>i)&1)//看state从右往左的第i位是不是1,是1说明要a的第k行的从右往做的第i个,也就是从左往右第nn-1-i个
            res+=a[k][nn-1-i];
    return res;
}
int dp_9_1()
{
    while(scanf("%d",&nn)!=EOF)
    {
        for(int i=0;i<nn;i++)
            for(int j=0;j<nn;j++)
                scanf("%d",&a[i][j]);

        memset(sum,0,sizeof(sum));
        memset(dp,0,sizeof(dp));

        /*数据处理,求出所有取值和*/
        int len=0; //每一行不冲突有len种状态可能
        for(int i=0;i<(1<<nn);i++)//枚举所有状态
            if(!(i&(i>>1)))//判断本行状态,要求横向不冲突,每一行其实是一样的要求
                sta[len++]=i;

        for(int i=0;i<nn;i++)//求第i行取第j个状态的取值总和
            for(int j=0;j<len;j++)
                sum[i][j]+=calculate(i,sta[j]);//第i行,用状态j的话,这一行的sum为多少

        /*状态转移,求最大和*/
        for(int i=0;i<len;i++)
            dp[0][i]=sum[0][i];//第一行,没有上面一行,所以就是sum
        for(int i=1;i<nn;i++)//行数
            for(int j=0;j<len;j++)//本行状态
                for(int k=0;k<len;k++)//上一行状态
                    if(!(sta[j]&sta[k]))//如果上一行,和当前行不冲突
                        dp[i][j]=max(dp[i][j],dp[i-1][k]+sum[i][j]);

        /*寻找最大和*/ //找dp[nn-1]里最大的那个
        int res=dp[nn-1][0];
        for(int i=1;i<len;i++)
            if(res<dp[nn-1][i])
                res=dp[nn-1][i];
        printf("%d\n",res);
    }
    return 0;
}

胜利大逃亡(续)-壮压+bfs

http://acm.hdu.edu.cn/showproblem.php?pid=1429
//并不是所有的都可以走回头路!只有添加了新钥匙才能走回路!所以也就是说visited不再是1维了,而应该是三维度,
// 第3个维度应该表示的是a-j有没有拿到,正常考虑用vector<int>(10)表示有么有拿到,但是这样visited就不是3维度了,是12维度了。
// 因此让1111111111这个数表示有没有拿到这个钥匙,也就是2^10=1024,拿到钥匙和才能走回头路!
// 我自己之前的做法是,queue保留当前点+钥匙状态,每一个点都可以走回头路,不可以
//第3维度也不可以是10,这样不能用一个点来表示状态,需要用10个点来表示状态。那么这个visited就不能表示当前这个状态有没有遍历过了。遍历到这个点其实可能有2^10状态
//0-1111111111 分别代表一个状态,
const int MAX=25;
struct point
{
    int x,y,step,state;
    point(int x=0,int y=0,int step=0,int state=0):x(x),y(y),step(step),state(state){};
};
char g[MAX][MAX];
int vis[MAX][MAX][1025];
int dirx[4]={0,1,0,-1};
int diry[4]={1,0,-1,0};
int n,m,t;
int sx,sy,ex,ey;
bool judgein(int x,int y)
{
    return 0<=x&&x<n&&0<=y&&y<m;
}
int bfs()
{
    memset(vis,0,sizeof(vis));
    queue<point> que;
    vis[sx][sy][0]=1;
    que.push(point(sx,sy,0,0));
    while(!que.empty())
    {
        point top=que.front();
        que.pop();
        int x=top.x;
        int y=top.y;
        int step=top.step;
        int state=top.state;
        if(step>=t)
        {
            return -1;
        }
        if(x==ex&&y==ey)
        {
            return step;
        }
        for(int i=0;i<4;i++)
        {
            int nx=x+dirx[i];
            int ny=y+diry[i];
            int nstep=step+1;
            int nstate=state;
            if(judgein(nx,ny)&&g[nx][ny]!='*'&&!vis[nx][ny][nstate])
            {
                if(g[nx][ny]=='.')
                {
                    vis[nx][ny][nstate]=1;
                    que.push(point(nx,ny,nstep,nstate));
                }
                else if(g[nx][ny]>='A'&&g[nx][ny]<='J')
                {
                    int key=nstate&(1<<(g[nx][ny]-'A'));
                    if(key)
                    {
                        vis[nx][ny][nstate]=1;
                        que.push(point(nx,ny,nstep,nstate));
                    }
                }
                else if(g[nx][ny]>='a'&&g[nx][ny]<='j')
                {
                    nstate=nstate|(1<<(g[nx][ny]-'a'));
                    vis[nx][ny][nstate]=1;
                    que.push(point(nx,ny,nstep,nstate));
                }
            }
        }
    }
    return -1;
}
int main_ok()
{
    while(scanf("%d%d%d",&n,&m,&t)!=EOF)
    {
        getchar();
        for(int i=0;i<n;i++)
        {
            for(int j=0;j<m;j++)
            {
                scanf("%c",&g[i][j]);
                if(g[i][j]=='@')
                {
                    sx=i; sy=j; g[i][j]='.';
                }
                if(g[i][j]=='^')
                {
                    ex=i; ey=j;  g[i][j]='.';
                }
            }
            getchar();
        }
        int ans=bfs();
        printf("%d\n",ans);
    }
    return 0;
}

 

2、插头dp

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值