算法提高课 第一章 动态规划①(数字三角形、最长上升子序列模型)

提高课第一章 动态规划:数字三角形、最长上升子序列、状态机模型、状态压缩DP、区间DP、树形DP

1.1数字三角形模型

1018. 最低通行费

image

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

using namespace std;

const int N = 105;

int g[N][N],n;
int f[N][N];//f[i][j]:g[1][1]到g[i][j]中所有方案中花费最小的方案
int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ )
    {
        for (int j = 1; j <= n; j ++ )
        {
            scanf("%d", &g[i][j]);
        }
    }
    
    memset(f,0x3f3f3f3f,sizeof f);//由于求最小值,初始化一定为无穷大
    for (int i = 1; i <= n; i ++ )
    {
        for (int j = 1; j <= n; j ++ )
        {
            if(i==1 && j==1) f[i][j] = g[i][j];//注意:左上角边界一定要特判
            else f[i][j] = min(f[i-1][j],f[i][j-1]) + g[i][j];
        }
    }
    cout<<f[n][n]<<endl;
    return 0;
}

1027. 方格取数

image

本题不同于前几道模型,需要求出走两次取得的最大值
走两次:f[i1,j1,i2,j2]表示从(1,1),(1,1)分别走到(i1,j1),(i2,j2)的路径的最大值
如何处理“同一个格子不能被重复选择”?
只有在i1 + j1 == i2 + j2时,两条路径的格子才可能重合
f[k,i1,i2]表示所有从(1,1),(1,1)分别走到(i1,k-i1),(i2,k-i2)的路径的最大值
k表示两条路线当前走到的格子的横纵坐标之和 k = i1 + j1 = i2 + j2
image

四维DP

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

using namespace std;

const int N = 15;

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

int main()
{
    cin>>n;
    while(cin>>a>>b>>c,a||b||c)
    {
        g[a][b] = c;
    }
    for(int x1 = 1;x1<=n;x1++)
    {
        for(int y1 = 1;y1<=n;y1++)
        {
            for(int x2 = 1;x2<=n;x2++)
            {
                for(int y2=1;y2<=n;y2++)
                {
                    int t = g[x1][y1];
                    if(x1!=x2 && y1!=y2)
                    {
                        t += g[x2][y2];
                    }
                    int &a = f[x1][y1][x2][y2];
                    a = max(a,f[x1-1][y1][x2-1][y2] + t);
                    a = max(a,f[x1-1][y1][x2][y2-1] + t);
                    a = max(a,f[x1][y1-1][x2][y2-1] + t);
                    a = max(a,f[x1][y1-1][x2-1][y2] + t);
                }
            }
        }
    }
    cout<<f[n][n][n][n]<<endl;
}

三维DP

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

using namespace std;

const int N = 15;

int w[N][N];
int f[2*N][N][N],n; //f[k][x1][x2]:从(1,1),(1,1)到(x1,y1)和(x2,y2)路径中最大数字和的路径,k为x1+y1=x2+y2
int a,b,c;
int main()
{
    cin>>n;
    while(cin>>a>>b>>c,a||b||c)
    {
        w[a][b] = c;
    }
    for (int k = 2; k <= 2*n; k ++ ) //枚举k,k相同时才有可能发生路径重合
    {
        for(int x1 = 1;x1<=n;x1++)//枚举x1,有k和x1就能推出y1
        {
            for(int x2 = 1;x2<=n;x2++)//枚举x2,有k和x2就能推出y2
            {
                int y1 = k - x1,y2 = k - x2;
                if(y1<1||y1>n||y2<1||y2>n) continue;//y1、y2必须在合法范围内
                int t = w[x1][y1];
                if(y1!=y2) t += w[x2][y2]; //路径未重合时,相加;重合时,不加
                int &x = f[k][x1][x2];//用x简化代码
                x = max(x,f[k-1][x1-1][x2-1] + t);//两条路都从上边来
                x = max(x,f[k-1][x1][x2] + t);//两条路都从左边来
                x = max(x,f[k-1][x1-1][x2] + t);//第一条从上边,第二条从左边
                x = max(x,f[k-1][x1][x2-1] + t);//第一条从左边,第二条从上边
            }
        }
    }
    cout<<f[2*n][n][n]<<endl;
    return 0;
}

1.2 最长上升子序列模型(一)

image

裸题模板:895. 最长上升子序列

#include<iostream>
#include<cstring>
#include<stack>
using namespace std;
const int N = 1005;
int n,a[N],ans;
int f[N];//所有以i结尾的严格单调上升子序列的集合
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        f[i] = 1;//初始化,最短子序列为1
    }
   for(int i=2;i<=n;i++)//右端点
   {
       for(int j = 1;j<=i;j++)
       {
           if(a[j] < a[i]) f[i] = max(f[i],f[j] + 1);//如果a[i]左边有比它小的数,选取个数较多的集合
           ans = max(ans,f[i]);//结果方案不一定包含最后一个数,因此需要取最大值
       }
   }
   cout<<ans<<endl;
   return 0;
}

1017. 怪盗基德的滑翔翼

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

using namespace std;

const int N = 105;

int h[N],f[N],n,t;
int ans = -1;

int main()
{
    scanf("%d", &t);
    while (t -- )
    {
        ans = -1;
        scanf("%d", &n);
        for (int i = 1; i <= n; i ++ ) 
        {
            scanf("%d", &h[i]);
        }
        for (int i = 1; i <= n; i ++ ) //正向求解LIS
        {
            f[i] = 1;
            for (int j = 1; j < i; j ++ )
            {
                if(h[i] > h[j]) f[i] = max(f[i],f[j] + 1);
                ans = max(ans,f[i]);
            }
        }
        for(int i = n;i;i--) //反向求解LIS
        {
            f[i] = 1;
            for(int j = n;j>i;j--) //注意循环顺序
            {
                if(h[i] > h[j]) f[i] = max(f[i],f[j] + 1);
                ans = max(ans,f[i]);
            }
        }
        cout<<ans<<endl;
    }
    return 0;
}

1014. 登山

image

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

using namespace std;

const int N = 1005;

int a[N],n;
int fl[N]; //fl[i]:从左到右,以i为结尾的最长上升子序列的值
int fr[N];//fr[i]:从右到左,以i为结尾的最长上升子序列的值
int ans;

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ )
    {
        scanf("%d", &a[i]);
        
    }
    for (int i = 1; i <= n; i ++ ) //计算fl[i]
    {
        fl[i] = 1;
        for (int j = 1; j < i; j ++ )
        {
            if(a[i] > a[j])
            {
                fl[i] = max(fl[i],fl[j] + 1);
            }
        }
    }
    for(int i = n;i;i--) //计算fr[i]
    {
        fr[i] = 1;
        for(int j = n;j>i;j--)
        {
            if(a[i] > a[j])
            {
                fr[i] = max(fr[i],fr[j] + 1);
            }
        }
        ans = max(ans,fl[i] + fr[i] - 1);//注意fl和fr计算i了两次,所以要减1
    }
    cout<<ans<<endl;
    return 0;
}

1012. 友好城市

image
对其中一个岸的城市按编号递增排序,再求另一个岸的LIS

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

using namespace std;
typedef pair<int, int> PII;

const int N = 5005;

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

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ )
    {
        scanf("%d%d", &a[i].first, &a[i].second);
    }
    sort(a+1,a+n+1); //排序,保证一条岸的单调性
    for (int i = 1; i <= n; i ++ ) //求另外一个岸的LIS,保证桥梁不相交
    {
        f[i] = 1;
        for (int j = 1; j < i; j ++ )
        {
            if(a[i].second > a[j].second) f[i] = max(f[i],f[j] + 1);
        }
        ans = max(ans,f[i]);
    }
    cout<<ans<<endl;
    return 0;
}

1016. 最大上升子序列和

image

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

using namespace std;

const int N = 1005;

int a[N],n;
int f[N]; //f[i]:以a[i]为结尾的最长上升子序列和的最大值
int ans;

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ )
    {
        scanf("%d", &a[i]);
    }
    for (int i = 1; i <= n; i ++ )
    {
        f[i] = a[i];
        for (int j = 1; j <= n; j ++ )
        {
            if(a[i] > a[j])
            {
                f[i] = max(f[i],f[j] + a[i]);
            }
        }
        ans = max(ans,f[i]);
    }
    cout << ans << endl;
    return 0;
}

1.2 最长上升子序列模型(二)

1010. 拦截导弹

image
无需使用优先队列,因为子序列结尾数组一定是升序有序的

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

using namespace std;

const int N = 1005;

int n,a[N];
int f[N];
int g[N];//表示每个子序列结尾的数组
int ans,cnt;

int main()
{
    while(cin>>a[n]) ++n;
    for(int i = 0;i<n;i++) //求解LIS部分
    {
        f[i] = 1;
        for(int j = 0;j<i;j++) //注意:此题求解的是最长下降子序列
        {
            if(a[i] <= a[j]) f[i] = max(f[i],f[j]+1);
        }
        ans = max(ans,f[i]);
    }
    cout<<ans<<endl;
    
    for (int i = 0; i < n; i ++ )//求解贪心部分
    {
        int k = 0;
        while(k<cnt && a[i] > g[k]) ++k; //g数组一定升序有序的,找到大于等于a[i]的最小的子序列结尾
        g[k] = a[i];
        if(k>=cnt) ++cnt; //没有就需要再开一个序列
    }
    cout<<cnt<<endl;
    return 0;
}

187. 导弹防御系统

由于n=50较小,本题可以用上一题贪心+dfs暴搜

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

using namespace std;

const int N = 55;

int a[N],n;
int up[N];//表示所有上升子序列的末尾高度
int down[N];//表示所有下降子序列的末尾高度
int ans;

void dfs(int u,int cnt_u,int cnt_d) //u表示搜到第几个高度,cntu表示上升序列个数,cntd表示下降序列个数
{
    if(cnt_u+cnt_d>=ans) return; //剪枝
    if(u==n)//搜完了
    {
        ans = min(ans,cnt_u + cnt_d);//求最小值
        return;
    }
    //情况1:将当前数放到上升子序列中
    int k = 0;
    while(k<cnt_u && a[u] <= up[k]) ++k;//找到小于a[u]的最大的上升子序列末尾
    int t = up[k];//备份,用于恢复现场
    up[k] = a[u];//放入a[u]
    if(k<cnt_u) dfs(u+1,cnt_u,cnt_d);
    else dfs(u+1,cnt_u+1,cnt_d);//没找到,那就重开一个序列
    
    up[k] = t; //注意:恢复现场
    //情况2:将当前数放到下降子序列中
    k = 0;
    while(k<cnt_d && a[u] >= down[k]) ++k;//找到大于a[u]的最小的下降子序列末尾
    t = down[k];//备份,用于恢复现场
    down[k] = a[u];//放入a[u]
    if(k<cnt_d) dfs(u+1,cnt_u,cnt_d);
    else dfs(u+1,cnt_u,cnt_d+1);//没找到,那就重开一个序列
    down[k] = t;//注意:恢复现场
    return;
}
int main()
{
    while(cin>>n,n)
    {
        for(int i = 0;i<n;i++) cin>>a[i];
        ans = 0x3f3f3f3f;//求最小值,初始化为无穷大
        dfs(0,0,0);
        cout<<ans<<endl;
    }
    return 0;
}

272. 最长公共上升子序列

image
状态表示:
f[i][j]代表所有a[1 ~ i]和b[1 ~ j]中以b[j]结尾的公共上升子序列的集合;
f[i][j]的值等于该集合的子序列中长度的最大值;

状态计算(对应集合划分):
首先依据公共子序列中是否包含a[i],将f[i][j]所代表的集合划分成两个不重不漏的子集:
不包含a[i]的子集,最大值是f[i - 1][j];
包含a[i]的子集,将这个子集继续划分,依据是子序列的倒数第二个元素在b[]中是哪个数:
子序列只包含b[j]一个数,长度是1;
子序列的倒数第二个数是b[1]的集合,最大长度是f[i - 1][1] + 1;
子序列的倒数第二个数是b[j - 1]的集合,最大长度是f[i - 1][j - 1] + 1;
如果直接按上述思路实现,需要三重循环:

朴素做法(O(n^3)超时)

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

using namespace std;

const int N = 3010;

int a[N],b[N],n;
int f[N][N];//f[i][j]:所有由第一个序列的前i个字母,和第二个序列的前j个字母构成的,且以b[j]为结尾的公共上升子序列
int ans;

int main()
{
    cin>>n;
    for (int i = 1; i <= n; i ++ )
    {
        cin>>a[i];
    }
    for (int i = 1; i <= n; i ++ )
    {
        cin>>b[i];
    }
    for (int i = 1; i <= n; i ++ )
    {
        for (int j = 1; j <= n; j ++ )
        {
            f[i][j] = f[i-1][j];//所有不包含a[i]的公共上升子序列
            if(a[i] == b[j])
            {
                f[i][j] = max(f[i][j],1); //求LIS
                for(int k = 1;k<j;k++)
                {
                    if(b[k] < b[j])
                    {
                        f[i][j] = max(f[i][j],f[i][k] + 1);
                    }
                }
            }
        }
    }
    for (int i = 1; i <= n; i ++ ) //求最大值为答案
    {
        ans = max(ans,f[n][i]);
    }
    cout<<ans<<endl;
    return 0;
}

然后我们发现每次循环求得的maxv是满足a[i] > b[k]的f[i - 1][k] + 1的前缀最大值。
因此可以直接将maxv提到第一层循环外面,减少重复计算,此时只剩下两重循环。
最终答案枚举子序列结尾取最大值即可。

优化做法(O(n^2))

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

using namespace std;

const int N = 3010;

int a[N],b[N],n;
int f[N][N];//f[i][j]:所有由第一个序列的前i个字母,和第二个序列的前j个字母构成的,且以b[j]为结尾的公共上升子序列
int ans;

int main()
{
    cin>>n;
    for (int i = 1; i <= n; i ++ )
    {
        cin>>a[i];
    }
    for (int i = 1; i <= n; i ++ )
    {
        cin>>b[i];
    }
    for (int i = 1; i <= n; i ++ )
    {
        int maxv = 1;
        for(int j = 1;j<=n;j++)
        {
            f[i][j] = f[i-1][j];
            if(a[i] == b[j]) f[i][j] = max(f[i][j],maxv);
            if(a[i] > b[j]) maxv = max(maxv,f[i][j] + 1);
        }
    }
    for (int i = 1; i <= n; i ++ ) //求最大值为答案
    {
        ans = max(ans,f[n][i]);
    }
    cout<<ans<<endl;
    return 0;
}
posted @   安河桥北i  阅读(38)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示