Acwing算法—动态规划

目录

数字三角形模型

AcWing 898. 数字三角形

/*
动态规划:状态的表示与计算
状态表示:
f(i,j)表示所有从起点走到(i,j)的路径中,数字和最大的路径
(某种集合的某种属性)
状态计算(集合划分):
f(i,j)可分为从左上角走到(i,j)或者从右上角走到(i,j)
f(i,j)=max( f(i-1,j-1)+a[i][j] , f(i-1,j)+a[i][j] )
*/
#include<iostream>
using namespace std;
const int N=510,INF=1e9;

int n;
int a[N][N];
int f[N][N];

int main(){
    cin>>n;
    
    for(int i=1;i<=n;i++){//读入数字三角形,下标从1开始
        for(int j=1;j<=i;j++) cin>>a[i][j];
    }
// 因为数据中可能有负数,为了防止从边界外转移过来(全局变量默认为0),
// 所以要将边界外赋值为-INF    
    for(int i=0;i<=n;i++){
//为了在计算状态时不处理边界,将所有状态初始化为负无穷
        for(int j=0;j<=i+1;j++) f[i][j]=-INF;
    }    
    
    f[1][1]=a[1][1];
    
    for(int i=2;i<=n;i++){
        for(int j=1;j<=i;j++) f[i][j]=max(f[i-1][j-1]+a[i][j],f[i-1][j]+a[i][j]);
    }
    
    int res=-INF;
    for(int i=1;i<=n;i++) res=max(res,f[n][i]);
    
    cout<<res<<endl;
    return 0;
    
}
//倒序dp,不需要考虑边界问题
#include<iostream>
using namespace std;

const int N=510;
int f[N][N];
int n;

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

    for(int i=n;i>=1;i--){
        for(int j=i;j>=1;j--){
            f[i][j]=max(f[i+1][j],f[i+1][j+1])+f[i][j];
        }//全局变量默认初始化为0,所以最底层数据计算没有问题
    }
    cout<<f[1][1]<<endl;
}

AcWing 1015. 摘花生

/*
从集合角度考虑DP

状态表示:
某种集合的某种属性(属性一般为max,min,数量)
f(i,j)表示所有从(1,1)走到(i,j)的路线中摘到花生的最大值,最后所求恰好是f(n,m)

状态计算:
对应于集合划分,划分时注意不重不漏,不漏一定要满足,但是当所求属性为最值时划分集合是否重复不重要
很重要的划分依据:“最后”
本题根据最后一步(i,j)从哪儿来划分:从上面下来;从左边过来。两类取最大值

f(i,j) = max( f(i,j-1) , f(i-1,j) ) + w(i,j)
*/
#include<iostream>
#include<algorithm>
using namespace std;

const int N=110;

int n,m;
int w[N][N];
int f[N][N];

int main(){
    
    int T;
    cin>>T;
    while(T--){
        cin>>n>>m;
        for(int i=1;i<=n;i++){//状态计算时需要用到上一行,上一列的信息,下标从1开始便于处理边界情况
            for(int j=1;j<=m;j++){
                cin>>w[i][j];
            }
        }
//计算状态,线性顺序
//注意是否需要初始化,这道题计算状态时,行列下标都从1开始,那对于那些下标为0的行列来说
//如果未特意初始化,全局变量默认为0,一定要多考虑边界行列状态计算时是否会有影响
//这道题中因为是求最值,并且都为非负整数,所以不初始化没有影响
//会说这么多是希望注意,很多题在这一步时是不初始化是会有影响的
        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++){
                f[i][j]=max(f[i-1][j],f[i][j-1])+w[i][j];
            }
        }
        cout<<f[n][m]<<endl;
    }
    return 0;
}
//空间压缩
#include<cstring>
#include<iostream>
using namespace std;

const int N=110;

int n,m;
int w[N][N];
int f[N];

int main(){
    
    int T;
    cin>>T;
    while(T--){
        cin>>n>>m;
        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++){
                cin>>w[i][j];
            }
        }

        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++){
                f[j]=max(f[j],f[j-1])+w[i][j];
            }
        }
        cout<<f[m]<<endl;
        //空间压缩,由于多组数据,注意置0
        memset(f, 0, sizeof f);
    }
    return 0;
}

AcWing 1018. 最低通行费

/*
动态规划的表示:一般网格图f(i,j),线形图f(i),背包问题第一维是物品,第二维是体积。需要经验
与上一题摘花生相似,但上道题要求走时只能往下或右走,这道题呢?
由于题目中的时间限制,等价于走时不能往回走,也就是只能往下或右走
所以两道题非常相似,只是一个求最大值,一个求最小值
也因此,两者对边界问题的处理不同
*/
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 110, INF = 1e9;

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

int main()
{
    cin>>n;

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

/*
这里与上一题不同,上一道题因为求的是最大值,且均为非负整数,所以下标为0的行列不初始化,
默认为0没有任何问题,但这里求的是最小值,均为非负整数,所以如果下标为0的行列不初始化,
全局变量默认为0,则状态计算时所有状态全部为0那么如何初始化呢?由于求的是最小值,
所以下标为0的行列全部初始化为正无穷即可
*/
    for (int i = 0; i <= n; i ++ ) {
        f[i][0] = f[0][i] = INF;
    }
      
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
        {
/*
这里边界问题中对于f(1,1)要特别注意,上道题求最大值时,上左均为0,取最大值加自身权重后没有
任何问题,这里对均为正无穷的上左求最小值加自身权重任为正无穷,所以要单独把f(1,1)拿出来特判
*/
            if (i == 1 && j == 1) f[i][j] = w[i][j]; 
            else {
                f[i][j] = min(f[i - 1][j] + w[i][j], f[i][j - 1] + w[i][j]);
            }
        }

    cout<<f[n][n];

    return 0;
}
/*
空间压缩
*/
#include<iostream>
using namespace std;

const int N=110,INF=1e9;

int w[N][N],f[N];

int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++) cin>>w[i][j];
    }
    
    for(int i=0;i<=n;i++) f[i]=INF;
    
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            if(i==1&&j==1) f[j]=w[i][j];
            else{
                f[j]=w[i][j]+min(f[j],f[j-1]);
            }
        }
    }
    
    cout<<f[n]<<endl;
}

AcWing 1027. 方格取数

/*
状态表示:
上两道题都是走一次,这道题需要走两次,如何扩展到走两次呢?
走一次:f(i,j)表示所有从(1,1)走到(i,j)的路径的最大值
走两次:
可以两条路线同时走,可能会走过相同的格子,但对于这些格子的数只能计算一次
f(i1,j1,i2,j2)表示所有从(1,1)分别走到(i1,j1),(i2,j2)的路径的最大值
因为是同时走,所以第一条路线走k步到达(i1,j1),第二条路线同样走k步到达(i2,j2),因此
i1+j1=i2+j2=k
所以可以降维到三维
f(i1,j1,i2,j2)变为f(k,i1,i2),其中j1=k-i1,j2=k-i2

状态计算:
对于每条路线,各自都可以向下或向右走到(i1,j1),(i2,j2),所以两两组合,有四种情况可以划分集合
以及要特别注意,计算每个状态时,如果当前两条路线走到了相同的点,那么这个点的数只计算一次
*/
#include<iostream>
#include<algorithm>
using namespace std;

const int N=15;

int n;
int w[N][N];
int f[2*N][N][N];

int main(){
    cin>>n;
    int a,b,c;
    while(cin>>a>>b>>c,a||b||c) w[a][b]=c;//输入到a,b,c均为0时停止,所以是||
    
    for(int k=2;k<=n+n;k++){
        for(int i1=1;i1<k;i1++){
            for(int i2=1;i2<k;i2++){
                int t=w[i1][k-i1];
                if(i1!=i2) t+=w[i2][k-i2];//判断是否走到同一格子,因为同时走,必然同时到
                int& x=f[k][i1][i2];
                x=max(x,f[k-1][i1-1][i2-1]+t);
                x=max(x,f[k-1][i1-1][i2]+t);
                x=max(x,f[k-1][i1][i2-1]+t);
                x=max(x,f[k-1][i1][i2]+t);
            }
        }
    }
    
    cout<<f[n+n][n][n]<<endl;
    return 0;
}

AcWing 275. 传纸条

/*
这道题与方格取数问题只有一点不同,那就是在方格取数中,同一个格子可以重复走,只是格子的数只能被计算一次,而这道题是不能走相同的格子
但是这道题完全能够用方格取数的代码解决是因为不管在那道题中,最优解永远不会由两条相交的路径组成,两条路径相交时,一定能绕路找到更优的解
*/
#include <iostream>

using namespace std;

const int N = 55;

int n, m;
int w[N][N];
int f[N * 2][N][N];

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

    for(int k=2;k<=n+m;k++){
        for(int i1=1;i1<k;i1++){
            for(int i2=1;i2<k;i2++){
                int t=w[i1][k-i1];
                if(i1!=i2) t+=w[i2][k-i2];
                int& x=f[k][i1][i2];
                x=max(x,f[k-1][i1-1][i2-1]+t);
                x=max(x,f[k-1][i1-1][i2]+t);
                x=max(x,f[k-1][i1][i2-1]+t);
                x=max(x,f[k-1][i1][i2]+t);
            }
        }
    }

    cout<<f[n + m][n][n];//注意这里都是n因为是i1,i2的范围

    return 0;
}

最长上升子序列模型

AcWing 895. 最长上升子序列

/*
DP
状态表示:
f[i]表示所有以第i个数结尾的上升子序列中长度最大的上升子序列的长度
满足某种条件的某种集合的某种属性
状态计算:
集合划分,这道题f[i]中第i个数一定存在于上升子序列中,
所以根据它的前一个数去分类,前一个数可能没有,可能是数组中第一个元素,
可能是数组中第二个元素,一直到可能是数组第i-1个元素
f[i]=max(f[j]+1) j=0,1,2...i-1;
f[0]到f[i-1]不一定都能取,因为可能有某个数大于f[i]
时间复杂度:O(n^2)
*/
#include<iostream>
using namespace std;
const int N=1010;

int n;
int a[N],f[N];

int main(){
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    int res=0;
    for(int i=1;i<=n;i++){
        f[i]=1;//只要a[i]一个数
        for(int j=1;j<i;j++){
            if(a[j]<a[i]) f[i]=max(f[i],f[j]+1);
        }
        res=max(res,f[i]);
    }
    
    cout<<res<<endl;
    return 0;
}
//记录转移过程
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int N=1010;

int n;
int a[N],f[N],g[N];//g[N]存储转移过程
vector<int> res;

int main(){
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    
    for(int i=1;i<=n;i++){
        f[i]=1;//只要a[i]一个数
        g[i]=0;//表示第i个数没有从谁转移过来
        for(int j=1;j<i;j++){
            if(a[j]<a[i]) {
                if(f[i]<f[j]+1){
                    f[i]=f[j]+1;//记录f[i]是从谁转移过来的
                    g[i]=j;
                }
            }
        }
    }
    
    int k=0;//存储最大值的下标
    for(int i=1;i<=n;i++){
        if(f[k]<f[i]) k=i;
    } 
    
    cout<<f[k]<<endl;
    
    while(f[k]){
        res.push_back(a[k]);
        k=g[k];
    }
    
    
    for(int i=res.size()-1;i>=0;i--) cout<<res[i]<<' ';
    
    
    return 0;
}

AcWing 896. 最长上升子序列 II

/*
与上道题一样,只是数据范围变大,上道题n^2做法会超时

考虑优化

依次遍历数组每个元素,将前面求得的最长上升子序列按长度分类
存储各个长度的上升子序列结尾的最小值,相同长度的上升子序列中,结尾更大的肯定
没有结尾较小的好,因为如果一个数可以接到较大的后面,也一定可以接到较小的后面
所以较大的没必要存下来,它是可被替换的,较小的适用范围更广

可证明各个长度的上升子序列结尾的最小值单调递增

当前遍历元素可以接到某一个上升子序列后面
换句话说,在不考虑边界情况的前提下,当前遍历元素可以替换这个递增的结尾序列中
第一个大于它的元素,也就是更新某个序列的结尾,
更新的意思是当前遍历的这个元素更适合当结尾
时间复杂度:对于每个元素二分查找 O(nlogn)
*/
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

int n;
int a[N];
int q[N];

int main()
{
    cin>>n;
    for (int i = 0; i < n; i ++ ) cin>>a[i];
//通用做法:两种二分查找都可以做,考虑边界情况
    q[0]=a[0];
//len表示数组q中最后一个元素的下标,因为从0开始,所有输出为len+1
    int len = 0;
    for (int i = 1; i < n; i ++ ){
// 先插入第一个元素,从第二个元素开始遍历
// 在结尾递增序列中找第一个大于等于当前遍历的元素,边界情况是最大元素都比它小
        if(q[len]<a[i]){//边界情况
            q[++len]=a[i];
        }
        else{
            int l = 0, r = len;
            while (l < r)
            {
                int mid = (l + r) >> 1;
                if (q[mid] >= a[i]) r= mid;
                else l = mid + 1;
            }
            q[l] = a[i];
        }
    }

    cout<<len+1<<endl;
    return 0;
}
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

int n;
int a[N];
int q[N];

int main()
{
    cin>>n;
    for (int i = 0; i < n; i ++ ) cin>>a[i];
//通用做法:两种二分查找都可以做,考虑边界情况
    q[0]=a[0];
//len表示数组q中最后一个元素的下标,因为从0开始,所有输出为len+1
    int len = 0;
    for (int i = 1; i < n; i ++ ){
// 先插入第一个元素,从第二个元素开始遍历
//在结尾递增序列中找最后一个小于当前遍历的元素,边界情况是最小元素都比它大
        if(q[0]>=a[i]) q[0]=a[i];
        else{
            int l = 0, r = len;
            while (l < r)
            {
                int mid = l + r +1>> 1;
                if (q[mid] < a[i]) l = mid;
                else r = mid - 1;
            }
            len=max(len,l+1);
            q[l+1] = a[i];            
        }
    }

    cout<<len+1<<endl;
    return 0;
}
#include <iostream>

using namespace std;

const int N = 100010;

int n,a[N],q[N];//存不同长度上升子序列的结尾最小值

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

    int len = 0;//len表示数组q中有多少个不同长度的序列最小值
    for (int i = 0; i < n; i ++ )
    {//遍历,二分查找数组q中有多少个不同长度的序列最小值小于当前遍历元素的元素
        int l = 0, r = len;
        while (l < r)
        {
            int mid = (l + r + 1) >> 1;
            if (q[mid] < a[i]) l = mid;
            else r = mid - 1;
        }

        len = max(len, r + 1);
        q[r + 1] = a[i];
    }

    cout<<len<<endl;
    return 0;
}

AcWing 1017. 怪盗基德的滑翔翼

/*
确定滑行方向后就转化为了LIS问题,原问题相当于正向和反向以ai为结尾的最长上升子序列长度, 
分别正向和反向各进行一次LIS,取得最大值即可。
*/
#include<iostream>
// #include<algorithm>
// #include<cstring>
using namespace std;

const int N=110;

int n;
int a[N],f[N];

int main(){
    int T;
    cin>>T;
    
    while(T--){
        cin>>n;
        for(int i=0;i<n;i++) cin>>a[i];
        
        int res=0;
        for(int i=0;i<n;i++){
            f[i]=1;
            for(int j=0;j<i;j++){
                if(a[i]>a[j]) f[i]=max(f[i],f[j]+1);
            }
            res=max(res,f[i]);
        }
        
        // memset(f,0,sizeof f);
        for(int i=n-1;i>=0;i--){
            f[i]=1;
            for(int j=n-1;j>i;j--){
                if(a[i]>a[j]) f[i]=max(f[i],f[j]+1);
            }
            res=max(res,f[i]);
        }   
        
        cout<<res<<endl;
    }
    return 0;
}
#include<iostream>
#include<cstring>
using namespace std;

const int N=110;

int n;
int a[N],f[N];

int main(){
    int T;
    cin>>T;
    while(T--){
        cin>>n;
        for(int i=0;i<n;i++) cin>>a[i];
        
        int len=0;
        for(int i=0;i<n;i++){
            int l=0,r=len;
            while(l<r){
                int mid=(l+r+1)>>1;
                if(f[mid]<a[i]) l=mid;
                else r=mid-1;
            }
            f[l+1]=a[i];
            len=max(len,l+1);
        }

        memset(f,0,sizeof f);
        
        int len_2=0;
        for(int i=n-1;i>=0;i--){
            int l=0,r=len_2;
            while(l<r){
                int mid=(l+r+1)>>1;
                if(f[mid]<a[i]) l=mid;
                else r=mid-1;
            }
            f[l+1]=a[i];
            len_2=max(len_2,l+1);
        }
        
        memset(f,0,sizeof f);
        
        int res=(len_2>len)?len_2:len;
        cout<<res<<endl;
    }
    return 0;
}

AcWing 1014. 登山

/*
求先严格上升再严格下降的最长子序列的长度,根据哪个点为峰值去分类
先正序求以每个点a[i]为结尾的最长上升子序列的长度,再逆序求以同一点a[i]为结尾的最长上升子序列的长度
两者相加减1取最大值(因为a[i]取了两次),上道题是两个序列整体求最大值
*/
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;

int n;
int a[N];
int f[N], g[N];

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

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

    for (int i = n; i >= 1; i -- )
    {
        g[i] = 1;
        for (int j = n; j > i; j -- )
            if (a[i] > a[j])
                g[i] = max(g[i], g[j] + 1);
    }

    int res = 0;
    for (int i = 1; i <= n; i ++ ) res = max(res, f[i] + g[i] - 1);

    cout<<res;

    return 0;
}
#include<iostream>
#include<cstring>
using namespace std;

const int N=1010;

int n;
int a[N],f[N],q[N],p[N];

int main(){
    cin>>n;
    for(int i=0;i<n;i++) cin>>a[i];
    
    int len=0;
    for(int i=0;i<n;i++){
        int l=0,r=len;
        while(l<r){
            int mid=(l+r+1)>>1;
            if(f[mid]<a[i]) l=mid;
            else r=mid-1;
        }
        f[l+1]=a[i];
        len=max(len,l+1);
        q[i]=l+1;
    }

    memset(f,0,sizeof f);
    
    len=0;
    for(int i=n-1;i>=0;i--){
        int l=0,r=len;
        while(l<r){
            int mid=(l+r+1)>>1;
            if(f[mid]<a[i]) l=mid;
            else r=mid-1;
        }
        f[l+1]=a[i];
        len=max(len,l+1);
        p[i]=l+1;
    }   
    
    int res=0;
    for(int i=0;i<n;i++) res=max(res,q[i]+p[i]-1);
    
    cout<<res<<endl;
    return 0;
}

AcWing 482. 合唱队形

/*
上一道登山的题的对偶,最少去掉多少人,就是最多剩下多少人,也就是求先上升再下降的最长子序列的长度,改一下数据范围和输出就行
*/
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 110;

int n;
int a[N];
int f[N], g[N];

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

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

    for (int i = n; i >&#
  • 4
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值