数字三角形模型

摘花生

  • 思路:
    1. 根据数据范围以及求最大值我们可以得知是动态规划的题目
    2. 状态表示:f[i][j]:走到(i,j)能够摘到的花生的集合。属性:最大值
    3. 状态转移:用当前的状点更新下一个点:f[nex][ney] = max(f[nex][ney], f[i][j] + a[nex][ney])
  • 代码:
#include <bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
const int N = 1e2 + 10;
const int INF = 0x3f3f3f3f3f3f3f3f;
int a[N][N], f[N][N];
int dx[] = {0, 1};
int dy[] = {1, 0};
signed main()
{
    int t;
    cin >> t;
    while(t --)
    {
        memset(f, 0, sizeof f);//初始化f为0
        int n, m;
        cin >> n >> m;
        for(int i = 1; i <= n; ++ i)
        {
            for(int  j = 1; j <= m; j ++) cin >> a[i][j];
        }
        f[1][1] = a[1][1];
        for(int i = 1; i <= n; i ++)
        {
            for(int j = 1; j <= m; j ++)
            {
                //遍历向右,向左的合法位置
                for(int k = 0; k < 2; k ++)
                {
                    int nex = i + dx[k], ney = j + dy[k];
                    if(nex < 0 || nex > n || ney < 0 || ney > m) continue;
                    f[nex][ney] = max(f[nex][ney], f[i][j] + a[nex][ney]);
                }
            }
        }
        cout << f[n][m] << endl;
    }
    return 0;
}

最低通行费

  • 思路:
    1. 大体思路同摘花生
    2. 求的最小值,所以f应该初始化为INF
  • 代码
#include <bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
const int N = 1e2 + 10;
const int INF = 0x3f3f3f3f3f3f3f3f;
int a[N][N], f[N][N];
int dx[] = {0, 1};
int dy[] = {1, 0};
signed main()
{
    memset(f, 0x3f, sizeof f);
    int n;
    cin >> n;
    for(int i = 1; i <= n; ++ i)
    {
        for(int  j = 1; j <= n; j ++) cin >> a[i][j];
    }
    f[1][1] = a[1][1];
    for(int i = 1; i <= n; i ++)
    {
        for(int j = 1; j <= n; j ++)
        {
            for(int k = 0; k < 2; k ++)
            {
                int nex = i + dx[k], ney = j + dy[k];
                if(nex < 0 || nex > n || ney < 0 || ney > n) continue;
                f[nex][ney] = min(f[nex][ney], f[i][j] + a[nex][ney]);
            }
        }
    }
    cout << f[n][n] << endl;
    return 0;
}

方格取数

  • 思路:

    1. 类似于摘花生走了两边。
    2. 首先想到跑两次摘花生的dp,第一次 DP 的结果会对第二次 DP 产生影响,即第一次可以获得最优解,但第二次不一定是最优解。最后答案也不是最优解,所以不能跑两次DP求解。
    3. 所以我们想到同时走,f[i1][j1][i1][j2]: 表示从(1, 1) 分别走到(i1,j1),(i2, j2)的最大数值之和
    4. 其实同时走的话,每次走的步数之和是一样的所以我们可以优化成三维,用k表示i + j,表示走了几步,这时j = k - i, 所以我们就可以用k和i表示j。
    5. 注意,当两次走到同一个点的时候,该点的数值只能加一次
    6. 状态表示:f[k][i1][i2],表示两次分别走k步,走到(i1, k- i1),(i2, k-i2) 的数值集合,属性:最大值。
    7. 状态转移: 用四个走法分别更新,(第一次,第二次)走的方向:(下下),(下右),(右下),(右右)
  • 代码:

#include <bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
const int N = 20;
const int INF = 0x3f3f3f3f3f3f3f3f;
int a[N][N];
int f[N * 2][N][N];
signed main()
{
    int n;
    cin >> n;
    int x, y, z;
    while(cin >> x >> y >> z && (x != 0 || y != 0 || z != 0))
    {
        a[x][y] = z;
    }
    for(int k = 2; k <= n * 2; k ++)
    {
        for(int i1 = 1; i1 <= n; i1 ++)
        {
            for(int i2 = 1; i2 <= n; i2 ++)
            {
                int j1 = k - i1, j2 = k - i2;
                if(j1 < 0 || j1 > n || j2 < 0 || j2 > n) continue;
                int sum = a[i1][j1];
                if(i1 != i2) sum += a[i2][j2];// 当两个人到达的不是同一个点的时候,我们要加两次这个点
                int &now = f[k][i1][i2];
                //下下
                now = max(now, f[k - 1][i1 - 1][i2 - 1] + sum);
                //下右
                now = max(now, f[k - 1][i1 - 1][i2] + sum);
                //右下
                now = max(now, f[k - 1][i1][i2 - 1] + sum);
                //右右
                now = max(now, f[k - 1][i1][i2] + sum);
            }
        }
    }
    cout << f[n * 2][n][n] << endl;
    return 0;
}

最长上升子序列模型

  1. 朴素版

    • 思路:

      ==状态表示:==f[i] 以第i个数结尾的上升子序列的集合

      ==集合划分:==我们以上升子序列的倒数第二数划分

      ==状态转移:==f[i] = max(f[i], f[j] + 1)

    • 代码实现:==复杂度O(N^2)==

    //状态表示:f[i], 以第i个数结尾的上升子序列的集合
    //状态转移:以序列的倒数第二个为多少进行划分
    #include<iostream>
    using namespace std;
    const int N = 1e3 + 10;
    int f[N], a[N];
    int main()
    {
        int n, res = -1;
        cin >> n;
        for(int i = 1; i <= n; i ++) cin >> a[i];
        for(int i = 1; i <= n; i ++)
        {
            f[i] = 1;//子序列最少为1
            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;
    }
    
  2. 二分优化

    • 思路:
    • 题解中最难理解的地方在于f中序列虽然递增,但是每个元素在原串中对应的位置其实可能是乱的,那为什么这个f还能用于计算最长子序列长度? 实际上这个f【不用于记录最终的最长子序列】,而是【以f[i]结尾的子串长度最长为i】或者说【长度为i的递增子串中,末尾元素最小的是f[i]】。理解了这个问题以后就知道为什么新进来的元素要不就在末尾增加,要不就替代第一个大于等于它元素的位置。 这里的【替换】就蕴含了一个贪心的思想,对于同样长度的子串,我当然希望它的末端越小越好,这样以后我也有更多机会拓展。
    • ==注意:==当要求的子序列不是严格递增的时候,即大于等于的时候,我们应该找==第一个大于x的数==进行替换,而不是大于等于,并且==只要大于等于f[cnt]==就可以加在f[cnt] 后面。
    • 实现代码:==复杂度(nlogn)==
    #include<iostream>
    using namespace std;
    const int N = 1e5 + 10;
    int f[N], a[N];
    int n, cnt = 1;
    int find(int x)
    {
        //查找第一个大于等于x的数
        int l = 1, r = cnt;
        while(l < r)
        {
            int mid = (l + r) / 2;
            if(f[mid] >= x) r = mid;
            else l = mid + 1;
        }
        return l;
    }
    int main()
    {
        cin >> n;
        for(int i = 1; i <= n; i ++) cin >> a[i];
    
        f[1] = a[1];
        for(int i = 2; i <= n; i ++)
        {
            if(a[i] > f[cnt]) 
            {
                f[++cnt] = a[i];
            }
            else
            {
                int idx = find(a[i]);
                f[idx] = a[i];
            }
        }
        cout << cnt << endl;
        return 0;
    }
    

怪盗基德的滑翔翼

  • 思路:
    1. 只能从高向低滑行并且要求经过的楼尽可能的多---> 最长上升子序列问题
    2. 可以随意选择方向,所以是双向的最长上升子序列问题
  • 代码:
#include <bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
const int N = 105;
const int INF = 0x3f3f3f3f3f3f3f3f;
int a[N], f[N];
signed main()
{
    int t;
    cin >> t;
    while(t --)
    {
        int n,  res = 0;
        cin >> n;
        for(int i = 1; i <= n; i ++) cin >> a[i];
        // 向右飞
        memset(f, 0, sizeof f);
        for(int i = 1; i <= n; i ++)
        {
            f[i] = 1;
            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]);
        }
        //向左飞
        memset(f, 0, sizeof f);
        for(int i = n; i >= 1; i --)
        {
            f[i] = 1;
            for(int j = n; 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;
}

登山

  • 思路:
    1. 根据题意序列应该成一个山峰的形状,所以我们可以从前向后和从后向前分别求一下最长上升子序列,然后枚举相交的转折点。
    2. 最后答案应该减一,转折点加了两次。
  • 代码:
#include <bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
const int N = 1005;
const int INF = 0x3f3f3f3f3f3f3f3f;
int a[N], f[N], ff[N];
signed main()
{
    int n;
    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[j] < a[i]) f[i] = max(f[i], f[j] + 1);
        }
    }
    //从后向前的上升子序列的集合
    for(int i = n ; i > 0; i --)
    {
        ff[i] = 1;
        for(int j = n; j > i; j --)
        {
            if(a[j] < a[i]) ff[i] = max(ff[i], ff[j] + 1);
        }

    }
    //枚举转折点,求得最大值
    int res = 0;
    for(int i = 1; i <= n; i ++)
    {
        res = max(res, f[i] + ff[i] -  1);
    }
    cout << res << endl;
    return 0;
}

友好城市

  • 思路:
    1. 为了保证尽可能建多的桥且不相交,我们应该让建桥两侧的楼的序位置是单调的,即可以求两侧都是单调上升时的最长子序列。
    2. 两侧同时考虑会有两个变量,我们考虑优化掉一个,可以先把一侧的位置进行排序,这样保证了一侧是单调上升的,然后我们再求另一侧的单调上升的子序列即可。
  • 代码:
#include <bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
const int N = 1e4 + 10;
const int INF = 0x3f3f3f3f3f3f3f3f;
typedef pair<int, int> PII;
vector<PII> p;
vector<int> a;
int f[N];
signed main()
{
    int n;
    cin >> n;
    for(int i = 0; i < n; i ++)
    {
        int x, y;
        cin >> x >> y;
        p.push_back({x, y});
    }
    sort(p.begin(), p.end());
    for(auto it : p)
    {
        a.push_back(it.second);
    }
    int res = 0;
    for(int i = 0; i < a.size(); i ++)
    {
        f[i] = 1;
        for(int j = 0; 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;
}

最大上升子序列的和

  • 思路:
    1. 最长上升子序列的变形,我们只需要在f[i]中存子序列的和就可以了
    2. 注意初始化:==f[i] = a[i]==
  • 代码:
#include <bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
const int N = 1e3 + 10;
const int INF = 0x3f3f3f3f3f3f3f3f;
int a[N], f[N];
signed main()
{
    int n, res = 0;
    cin >> n;
    for(int i = 1; i <= n; i ++) cin >> a[i];
    for(int i = 1; i <= n; i ++)
    {
        f[i] = a[i];
        for(int j = 1; j < i; j ++)
        {
            if(a[j] < a[i])
            {
                f[i] = max(f[i], f[j] + a[i]);
            }
        }
        res = max(res, f[i]);
    }
    cout << res << endl;
    return 0;
}

导弹拦截

  • 思路:

    1. 第一步求解最长不下降子序列。

    2. 贪心思想:(1) g[i]记录每个子序列的最后一个值,当有一个新的数加入进来时:

      ​ (2) 如果这个数大于所有的g[i], 我们就要新开一个序列, 否则选择大于等于x的最大的g[i] 进行替换。

    3. 容易得知,g[i] 应该是一个单调上升的子序列。

  • 结论:==求解最长不下降子序列的个数等于该序列的上升子序列的最大长度。==

  • 代码:

#include <bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
const int N = 1e3 + 10;
const int INF = 0x3f3f3f3f3f3f3f3f;
int a[N], f[N], g[N];
int n = 1;
signed main()
{
    while(cin >> a[n]) n ++;
    n --;
    //cout << n << endl;
    f[1] = a[1];
    int cnt = 1;
    for(int i = 2 ; i <= n; i ++)
    {
        if(a[i] <= f[cnt]) f[++ cnt] = a[i];
        else
        {
            // 替换第一个小于等于a[i]的数
            int l = 1, r = cnt;
            while(l < r)
            {
                int mid = (l + r) / 2;
                if(f[mid] <= a[i]) r = mid;
                else l = mid + 1;
            }
            f[l] = a[i];
        }
    }
    cout << cnt << endl;
    int now = 1;
    g[1] = a[1];
    for(int i = 2; i <= n; i ++)
    {
        if(a[i] > g[now]) g[++ now] = a[i];
        else
        {
            int l = 1, r = now;
            while(l < r)
            {
                int mid = (l + r) / 2;
                if(g[mid] >= a[i]) r = mid;
                else l = mid + 1;
            }
            g[l] = a[i];
        }
    }
    cout << now << endl;
    return 0;
}