数字三角形模型-最长上升子序列模型-ACWing(提高课思路整理)

数字三角形模型

1015:采花生

简单分析:输出网格左上角到右下角的路径中能够采到的最多的花生数;
设f[i][j]表示(i,j)走到坐标时能够采摘的最多的花生数目
固定(i,j),该坐标只能从(i, j - 1),(i - 1, j)两个点转移过来,只需要选择其中采摘花生数最大的一项加上当前的这个点上的花生数目就是走到(i, j)这个点最多可采的花生数。
f[i][j] =  f[i][j] = max(f[i-1][j], f[i][j-1]) + num[i][j];

f 数组最好存数从下标1开始,避免从0开始时存在的max(f[-1][0], f[0][-1])的数组越界情况

cpp写法

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

const int N = 110, M = 1010;

int f[N][N];
int num[N][N];

int t, r, c;

int main()
{
    cin >> t;
    while( t -- ){
        cin >> r >> c;
        memset(f, 0, sizeof f);
        for(int i = 1 ; i <= r; i ++){
            for(int j = 1 ; j <= c ; j ++){
                cin >> num[i][j]; 
            }
        }
        
        for(int i = 1; i <= r; i++){
            for(int j = 1; j <= c; j++){
                f[i][j] = max(f[i-1][j], f[i][j-1]) + num[i][j];
            }
        }
        cout << f[r][c] << endl;
    }
    return 0;
}

python3写法

def count(nums, n, m):
    '''计算到终点花了多少'''
    f = [[0 for _ in range(m + 1)] for _ in range(n + 1)]
    for i in range(1, n + 1):
        for j in range(1, m + 1):
            f[i][j] = max(f[i][j - 1], f[i - 1][j]) + nums[i - 1][j - 1]
    return f[n][m]

if __name__ == "__main__":
    '''
        最简单的dp,左上起点到右下终点
        用f[i][j]表示走到(i,j)最多采了几个
    '''
    t = int(input())
    for _ in range(t):
        n, m = map(int, input().split())
        nums = []
        for i in range(n):
            row = list(map(int, input().split()))
            nums.append(row)
        print(count(nums, n, m))

最低通行费用

采花生方案的另外一种应用,从目标球最大改成求最小。

c++写法:

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

using namespace std;

const int N = 110;
const int INF = 0x3f3f3f3f;

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

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

python3写法:

if __name__ == "__main__":
    '''
        本质和采花生一致,因为需要确保可以在2N-1的时间走出去,那么就只能向下或向右走
        现在的目标是求最少的费用
    '''
    n = int(input())
    cost = []
    for _ in range(n):
        row = list(map(int, input().split()))
        cost.append(row)
    f = [[ 0x3f3f3f3f for _ in range(n + 1)] for _ in range(n + 1)]
    for i in range(1, n + 1):
        for j in range(1, n + 1):
            if i == 1 and j == 1:
                f[i][j] = cost[i - 1][j - 1]
            else:
                f[i][j] = min(f[i - 1][j], f[i][j - 1]) + cost[i - 1][j - 1]
    print(f[n][n])

方格取数

简单分析:
	左上走到右下,只能向下和向右走,走过一个有数的方格就取数
    需要走两次,求两个路径取到的数字和最大
    其实从左上角派出两个小人来取数就行了,当两个小人同时踩到一个有数的方格时只取一个就行
    然后我们可以用把x,y两个轴合并成一个n+n的范围(因为在只能向右向下走的时候,我们的小人最多走2 * n步),并且我们是同时派出两个小人,所以两个小人的坐标之和是相同的,只需要枚举每个小人横坐标就可以知道当前这一步的移动的位置状态:
    我们可以用f[k][x1][x2]表示已经从左上角走了K步,且当前第一个人在(x1, k - x1)位置上,第二个人在(x2, k - x2)位置上最多可以取到的数之和。
    假设已经走了n步,两个小人的横坐标分别为x1, x2;对应纵坐标为 n - x1, n - x2
    当前状态能够取到的最多的数之和可以从上一次已经走了n-1步的情况推导来,n-1步到第n步这个状态可能由四种情况:
    1. 两个小人都向下  
    2. 第一个小人向下,第二个小人向右
    3. 第一个小人向右,第二个小人向下
    4. 两个小人都向右
只需要对四种状态转移求一个max,在加上当前这个状态可以取到数即为当前f[k][x1][x2]的值。
(当x1 == x2时则说明两个小人在同一个位置上,那么只会取一次数)

c++写法:

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

using namespace std;

const int N = 15;

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

int n;
int a, b ,c;

int main()
{
    cin >> n;
    memset(w, 0, sizeof w);
    while(cin >> a >> b >> c, a || b || c) 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 t = w[i1][j1];
                    if(i1 != i2){
                        t += w[i2][j2];
                    }
                    int &x = f[k][i1][i2];
                    x = max(x, f[k-1][i1-1][i2-1] + t);
                    x = max(x, f[k-1][i1][i2-1] + t);
                    x = max(x, f[k-1][i1-1][i2] + t);
                    x = max(x, f[k-1][i1][i2] + t);
                }
            }
        }
    }
    cout << f[2 * n][n][n];
    return 0;
}

python3写法:

if __name__ == "__main__":
    n = int(input())
    w = [[0] * (n + 1) for _ in range(n + 1)]
    a, b, c = map(int, input().split())
    while(a | b | c):
        w[a][b] = c
        a, b, c = map(int, input().split())
    f = [[[0] * (n + 1) for _ in range(n + 1)] for _ in range(2 * n + 1)]
    for k in range(2, 2 * n + 1):
        for x1 in range(1, n + 1):
            for x2 in range(1, n + 1):
                y1, y2= k - x1, k - x2
                if(y1 >= 1 and y1 <= n and y2 >= 1 and y2 <= n):
                    t = w[x1][y1]
                    if x1 != x2:
                        t += w[x2][y2]
                    f[k][x1][x2] = max(f[k-1][x1 - 1][x2 - 1], f[k - 1][x1 - 1][x2], f[k - 1][x1][x2 - 1], f[k - 1][x1][x2]) + t
    print(f[2 * n][n][n])

传纸条

简单分析:

    同样可以把题目抽象成矩阵图从左上角走到右下角,两个人一个从左上到右下,一个从右下到左上,等价于两个人都从左上走到右下,但是每一个格子只能走一次,希望两个路径能够从格子中取数最大。
    回顾上一题:同样可以用f[k][x1][x2]来表示当前走了k步的状态下,第一个人在(x1, k - x1), 第二个人在(x2, k - x2)这个位置上,此时需要分析最优的两条路径有什么关系:两条最优路径有可能会相互穿插,或者会相交,但是前者永远可以向第二者转移,所以只需要解决第二个情况。
    第二种情况最好画图分析一下,会发现如果我们把两个最优路线相较的地方,让一个路线绕一下,并不会影响最终结果,因为当路线相较,说明某个路线绕道的相邻点必然数值为0,不然必然存在相交点不是最优路径上的点,所以一旦遇到相交的点,当一个人绕路就行了,而且绕路取数为0不影响结果,所以就回归上一题,走到相同位置只取当前位置的数值一次就行。
其他的照搬上一道题即可

c++写法:

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

const int N = 55;

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

int main()
{
    cin >> m >> n;
    for(int i = 1; i <= m ; i ++){
        for(int j = 1; j <= n ; j++){
            cin >> w[i][j];
        }
    }
    memset(f, 0, sizeof f);
    for(int k = 2; k <= m + n ; 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 = f[k][i1][i2];
                    x = max(x, f[k-1][i1-1][i2-1] + t);
                    x = max(x, f[k-1][i1][i2-1] + t);
                    x = max(x, f[k-1][i1-1][i2] + t);
                    x = max(x, f[k-1][i1][i2] + t);
                }
            }
        }
    }
    cout << f[m + n][m][m];
    return 0;
}

python3写法:

if __name__ == "__main__":
    m, n = map(int, input().split())
    w = [[0] * (n + 1) for _ in range(m + 1)]
    for i in range(1, m + 1):
        temp = list(map(int, input().split()))
        for j in range(1, n + 1):
            w[i][j] = temp[j - 1]
    f = [[[0] * (m + 1) for _ in range(m + 1)] for _ in range(n + m + 1)]

    for k in range(2, n + m + 1):
        for x1 in range(1, m + 1):
            for x2 in range(1, m + 1):
                y1, y2 = k - x1, k - x2
                if 1 <= y1 <= n and 1 <= y2 <= n:
                    t = w[x1][y1]
                    if x1 != x2:
                        t += w[x2][y2]
                    f[k][x1][x2] = max(f[k - 1][x1 - 1][x2 - 1], f[k - 1][x1 - 1][x2], f[k - 1][x1][x2 - 1], f[k - 1][x1][x2]) + t
    print(f[n + m][m][m])

最长上升子序列模型

1017. 怪盗基德的滑翔翼

简单分析:

只能从建筑中某个点向某个方向飞行,并且只能从高飞向低,实际从x位置飞,最多能飞的建筑数目就是以x为起点向左或右的最长单调递减序列
所以本质上题目就是求一个序列当中最长上升子序列和最长下降子序列中的最大长度。
最长上升子序列框架:
f[i]: 表示到以i为结尾的最长上升子序列的长度
f[i] = max(f[k] + 1, f[i]) if q[k] > q[i] for k in range(i) 
因为要求两个:一个上升一个下降,做两遍求一个max就行了

c++写法:

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

const int N = 110;

int k, n;
int f[N], g[N];
int w[N];
int max_right, max_left;

int main()
{
    cin >> k;
    while(k --){
        cin >> n;
        max_right = 0, max_left = 0;
        for(int i = 0 ; i < n ; i++){
            cin >> w[i];
        }
        for(int i = 0 ; i < n ; i ++){
            int wi = w[i];
            for(int j = 0 ; j < i ; j++){
                int wj = w[j];
                if(wj > wi){
                    f[i] = max(f[i], f[j] + 1);
                }
            }
            if(f[i] == 0) f[i] = 1;
            max_right = max(max_right, f[i]);
        }
        for(int i = n-1 ;i >= 0; i--){
            int wi = w[i];
            for(int j = n - 1; j >= 0; j--){
                int wj = w[j];
                if( wj > wi){
                    g[i] = max(g[i], g[j] + 1);
                }
            }
            if(g[i] == 0) g[i] = 1;
            max_left = max(max_left, g[i]);
        }
        if(n == 0) cout << 0 << endl;
        else{
            cout << max(max_right, max_left) << endl;
        }
    }
    return 0;
}

python3写法:

if __name__ == "__main__":
    t = int(input())
    for _ in range(t):
        n = int(input())
        q = list(map(int, input().split()))
        f = [1] * n
        g = [1] * n
        ans = 0
        for i in range(n):
            f[i] = max([f[k] + 1 for k in range(i) if q[k] > q[i]], default=1)
            g[i] = max([g[k] + 1 for k in range(i) if q[k] < q[i]], default=1)
            ans = max(f[i], g[i], ans)
        print(ans)

1014. 登山

简单分析:

景点浏览编号顺序必须单调递增,同时浏览的方式是上山后就下山(严格单增后严格单减)
1:第一个条件限制了我们从某个景点开始,只能往这个景点之后的景点走(限制了暴力搜索的一个范围)
2:目的是希望求最多可以看多少个景点,结合上山单增,下山单减,本质上就是求最长单调子序列问题,上山就是最长上升的模板
   下山可以倒过来求,从最后一个编号开始求逆向的最长上升。
3:得到两个方向f数组之后,把某个景点作为中间景点(上山到下山的转折点)的最多可以访问多少景点 = f1[i] + f2[i]

c++写法:

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

using namespace std;

const int N = 1010;

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

int main()
{
    cin >> n;
    for(int i = 0 ; i < n ; i++){
        cin >> w[i];
    }
    for(int i = 0 ; i < n ; i ++){
        int wi = w[i];
        f[i] = 1;
        for(int j = 0 ; j < i ; j++){
            int wj = w[j];
            if(wj < wi){
                f[i] = max(f[i], f[j] + 1);
            }
        }
    }
    for(int i = n - 1; i >= 0; i--){
        int wi = w[i];
        g[i] = 1;
        for(int j = n - 1; j > i; j --){
            int wj = w[j];
            if(wj < wi){
                g[i] = max(g[i], g[j] + 1);
            }
        }
    }
    int res = 0;
    for(int i = 0 ; i < n ; i ++){
        res = max(res, f[i] + g[i] - 1);
    }
    cout << res;
    return 0;
}

python3写法

if __name__ == "__main__":
    n = int(input())
    q = list(map(int, input().split()))
    f1, f2 = [0] * n, [0] * n
    ans = 0
    for i in range(n):
        f1[i] = max([f1[k] + 1 for k in range(i) if q[k] < q[i]], default=1)
    for i in range(n - 1, -1, -1):
        f2[i] = max([f2[k] + 1 for k in range(n - 1, i, -1) if q[k] < q[i]], default=1)
        ans = max(f2[i] + f1[i] - 1, ans)
    print(ans)

482. 合唱队形

简单分析:

'''
题意要求计算最少需要出队几个人,但是合唱队形明显是先严格单增后严格单减,所以本质上可以计算一下合唱队形最多有多少人,那么出队就是总数-合唱队形人数
合唱队形的最多人数,实际就是求一下最长上升子序列问题,递减就是反向求最长上升。
套用模板即可。
'''
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 110;

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

int main()
{
    cin >> n;
    for(int i = 0 ; i < n; i++) cin >> w[i];
    memset(f, 0, sizeof f);
    memset(g, 0, sizeof g);

    for(int i = 0 ; i < n ; i++){
        int wi = w[i];
        f[i] = 1;
        for(int j = 0; j < i ; j++){
            int wj = w[j];
            if(wi > wj){
                f[i] = max(f[i], f[j] + 1);
            }
        }
    }
    for(int i = n - 1; i >= 0; i--){
        int wi = w[i];
        g[i] = 1;
        for(int j = n - 1; j > i ; j--){
            int wj = w[j];
            if( wi > wj){
                g[i] = max(g[i], g[j] + 1);
            }
        }
    }
    
    int k = 0;
    for(int i = 0 ; i < n ; i++){
        k = max(k, f[i] + g[i] - 1);
    }
    
    cout << n - k << endl;
    
    return 0;
}
if __name__ == "__main__":
	n = int(input())
	q = list(map(int, input().split()))
	up, down = [0] * n, [0] * n
	t = 0
	for i in range(n):
	   up[i] = max([up[k] + 1 for k in range(i) if q[k] < q[i]], default=1)
	
	for i in range(n - 1, -1, -1):
	   down[i] = max([down[k] + 1 for k in range(n - 1, i, -1) if q[k] < q[i]], default=1)
	   t = max(up[i] + down[i] - 1, t) # 注意因为在正向和逆向时,i这个位置的人计算了两次,所以要-1
	print(n - t)

1012. 友好城市

简单分析:
‘’’

    想要不相交的尽可能多,那么实际上就是可以固定一侧的编号,然后比较他们在对岸的友好城市的编号的,不相交那么就是严格递增
    所以可以用最大上升子序列来求整个序列的严格单增的最大长度,就是我们的最多通过数。
'''
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;

const int N = 5010;

int n;
int f[N];
struct City{
    int north;
    int south;
    bool operator< (const City &c) const{
        return c.north > north;
    }
};
vector<City> friends;

int main()
{
    cin >> n;
    for(int i = 0 ; i < n ; i ++){
        City temp;
        cin >> temp.south >> temp.north;
        friends.push_back(temp);
    }
    sort(friends.begin(), friends.end());
    // for(int i = 0 ;  i< n; i++){
    //     cout << friends[i].south << " ";
    // }
    int res = 0;
    for(int i = 0 ; i < n ; i++){
        int wi = friends[i].south;
        f[i] = 1;
        for(int j = 0 ; j < i; j++){
            int wj = friends[j].south;
            if(wj < wi){
                f[i] = max(f[i], f[j] + 1);
            }
        }
        res = max(res, f[i]);
    }
    cout << res;
    return 0;
}

python3写法:

if __name__ == '__main__':
    n = int(input())
    friends = [list(map(int, input().split())) for _ in range(n)]
    friends.sort(key= lambda x: x[0]) # 排个序是固定一岸的编号
    f = [0] * n
    ans = 0
    for i in range(n):
        f[i] = max([f[k] + 1 for k in range(i) if friends[k][1] < friends[i][1]], default=1)
        ans = max(ans, f[i])
    print(ans)

1016. 最大上升子序列和

模板题不分析

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

using namespace std;

const int N = 1010;

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

int main()
{
    cin >> n;
    for(int i = 0 ; i < n; i++){
        cin >> w[i];
    }
    memset(f, 0, sizeof f);
    int res = 0;
    for(int i = 0 ; i < n; i++){
        int wi = w[i];
        f[i] = w[i];
        for(int j = 0 ; j < i; j++){
            int wj = w[j];
            if(wj < wi){
                f[i] = max(f[i], f[j] + w[i]);
            }
        }
        res = max(res, f[i]);
    }
    cout << res;
    return 0;
}

python3写法:

if __name__ == '__main__':
    '''
    模板题:需要注意就是最大上升子序列和,其不一定是最长的
    '''
    n = int(input())
    q = list(map(int, input().split()))
    f = [0] * n
    ans = 0
    for i in range(n):
        f[i] = max([f[k] + q[i] for k in range(i) if q[k] < q[i]], default=q[i])
        ans = max(ans, f[i])
    print(ans)

1010. 拦截导弹

简单分析:

第一小问就是最简单的最长上升子序列的模板问题(只是这里求下降)
第二小题:
    求要拦截所有导弹最少要几个系统?
    方法1:继续用dp,因为每个系统都是递减往下打,那么序列中递增的导弹很显然需要用不同的系统来拦截,所以实际只需要再求一个最长上升子序列就是系统的数目
    方法2:使用贪心,假设有n个系统,遍历每一个导弹,我们都从1-n的顺序访问每一个系统,选择可以拦截当前导弹的系统中上一个拦截的导弹高度最小的系统进行拦截。
            如果没有任何一个系统可以拦截这个导弹就新开一个系统n+1。最后n的个数就是系统所需的答案。

c++:

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

using namespace std;

const int N = 1010;
int f[N], g[N];
int w[N];

int main()
{
   string line;
   getline(cin, line);
   stringstream ssin(line);
   int n = 0;
   while(ssin >> w[n]) n++;
   
   memset(f, 0, sizeof f);
   memset(g, 0, sizeof f);
   
   int res1 = 0;
   int res2 = 0;
   
   for(int i = 0 ; i < n ; i++){
       int wi = w[i];
       f[i] = 1;
       g[i] = 1;
       for(int j = 0 ; j < i ; j++){
           int wj = w[j];
           if(wj >= wi){
               f[i] = max(f[i], f[j] + 1);
           }
           if( wj < wi){
               g[i] = max(g[i], g[j] + 1);
           }
       }
       res1 = max(res1, f[i]);
       res2 = max(res2, g[i]);
   }
   
   cout << res1 << endl;
   cout << res2 << endl;
   
   return 0;
} 

python3:

if __name__ == '__main__':
   missiles = list(map(int, input().split()))
   n = len(missiles)
   f = [0] * n
   max_missile, systems, sys_id = 0, [0] * n, 0
   for i in range(n):
       f[i] = max([f[k] + 1 for k in range(i) if missiles[k] >= missiles[i]], default=1)
       max_missile = max(max_missile, f[i])
       k = 0
       while k < sys_id and systems[k] < missiles[i]:
           k += 1
       systems[k] = missiles[i]
       if k >= sys_id:
           sys_id += 1
   print(max_missile)
   print(sys_id)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值