【第十三届蓝桥杯国赛训练营第一周——动态规划】

🍟A 骰子的点数(简单)

在这里插入图片描述
dp[i][j],表示有 i 个骰子,掷出点数范围为[j…6j]的每个点数的掷法,那么对于dp[1],所有点数都只有一种方法。

当n=2时,如果要掷出点数3,组合方式=1 + 2 / 2 + 1,就两种,我们假设现在已经掷了一个骰子,点数为k,那么我们还需要去看另外一个骰子的情况,因为要让点数凑成3,所以就看dp[i - 1][3 - k],就是说看一个骰子掷出3-k点数的种类数。

也就是说,站在最后一个骰子的角度去考虑问题,考虑前面的骰子能够为这个骰子带来什么?

class Solution {
    public int[] numberOfDice(int n) {
        // ans[i][j] i个骰子,掷出j个点的每个点的方案数
        int[][] ans = new int[n + 1][6 * n + 1];
        // 一个骰子掷出1-6点数的种类都=1
        for (int i = 1; i <= 6; i++) {
            ans[1][i] = 1;
        }
        // 遍历n个骰子
        for (int i = 2; i <= n; i++) {
            // 遍历n个骰子的点数总和的范围
            for (int j = i; j <= i * 6; j++) {
                // 当前这个骰子投掷的点数大小
                for (int k = 1; k <= 6; k++) {
                    if (j - k > 0) {
                        // 当前这个骰子点数为k,那么前面i-1个骰子的点数之和只能为j - k
                        ans[i][j] += ans[i - 1][j - k];
                    }
                }
            }
        }
        return Arrays.copyOfRange(ans[n], n, 6 * n + 1);
    }
}

🥞B 挖地雷(简单)

在这里插入图片描述
在这里插入图片描述
站在当前地窖考虑,考虑前面有几个地窖能到达当前地窖,所以可以很容易得到dp数组的含义,dp[i],以第i个地窖结束的路径,挖得最多的地雷数,这个地雷数只是以第i个地窖结束的路径的最大值,并不是全局的,所以还要记录一个全局最大值,最后利用这个全局最大值,从尾到头依次遍历,寻找出路径。

需要注意的是,对于所有dp[i]的初始值,所有地窖都可以只考虑自己,所以初始值就是当前地窖的地雷数。

import java.util.*;
import java.io.*;

public class Main {
    static BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
    static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    public static void main(String[] args) throws IOException {
        Scanner scan = new Scanner(System.in);
        int n = scan.nextInt();
        int[] bomb = new int[n + 1];
        for (int i = 0; i < n; i++) {
            bomb[i + 1] = scan.nextInt();
        }
        boolean[][] vis = new boolean[n + 1][n + 1];
        while (true) {
            int x = scan.nextInt();
            int y = scan.nextInt();
            if (x == 0 && y == 0) break;
            vis[x][y] = true;
        }
        // dp[i],挖的最后一个地窖为i时,能够挖到的最大地雷数
        int[] dp = new int[n + 1];
        // dp[i]中每一个元素,只考虑自身地窖时,最大地雷数=bomb[i]
        for (int i = 1; i <= n; i++) {
            dp[i] = bomb[i];
        }
        int max = dp[1];
        // 遍历结束位置
        for (int i = 2; i <= n; i++) {
            // 可能的开始位置
            for (int j = 1; j < i; j++) {
                if (vis[j][i]) {
                    // j -> i 有通路
                    dp[i] = Math.max(dp[i], dp[j] + bomb[i]);
                }
            }
            // 记录全局最大值
            max = Math.max(max, dp[i]);
        }
        StringBuilder sb = new StringBuilder();
        int tmp = max;
        // 从后往前记录路径
        for (int i = n; i >= 1; i--) {
            if (dp[i] == tmp) {
                tmp -= bomb[i];
                if (tmp == 0) {
                    sb.insert(0, i);
                } else {
                    sb.insert(0, "-" + i);
                }
            }
        }
        System.out.println(sb.toString());
        System.out.println(max);
    }
}

🥙C 守望者的逃离(简单)

在这里插入图片描述
在这里插入图片描述
首先需要知道的是,直接走和使用技能哪个更快:
在这里插入图片描述
我们可以先算出来只使用技能的情况下,1-t秒的每一秒能走多远。但是这样考虑问题是不够全面的,当剩余距离很小时,就不需要再等技能恢复,直接跑就行,所以对于每一秒,我们还需要将:使用技能的距离 与 直接跑的距离 作比较,取其中的最大值作为最终的最大距离。

import java.util.*;
import java.io.*;

public class Main {
    static BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
    static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    public static void main(String[] args) throws IOException {
        String[] input = reader.readLine().trim().split(" ");
        int M = Integer.parseInt(input[0]);
        int S = Integer.parseInt(input[1]);
        int T = Integer.parseInt(input[2]);
        // dp[i], 第 is 能够到达的最远距离
        int[] dp = new int[T + 1];

        // 先求得只使用技能的每秒能够到达的最远距离
        for (int i = 1; i <= T; i++) {
            if (M >= 10) {
                // 魔力值允许释放技能就立马释放
                M -= 10;
                dp[i] = dp[i - 1] + 60;
            } else {
                // 魔力值不够释放技能,就原地等待
                M += 4;
                dp[i] = dp[i - 1];
            }
        }

        // 再求得穿插使用直接跑的情况下,每秒能够到达的最远距离
        for (int i = 1; i <= T; i++) {
            dp[i] = Math.max(dp[i], dp[i - 1] + 17);
        }

        // 两步处理完后得到的就是每一秒能够到达的最远距离
        boolean flag = false;
        for (int i = 1; i <= T; i++) {
            if (dp[i] >= S) {
                flag = true;
                System.out.println("Yes");
                System.out.println(i);
                break;
            }
        }
        if (!flag) {
            // 逃离不了就输出能够走的最远距离
            System.out.println("No");
            System.out.println(dp[T]);
        }
    }
}

※🍛D 旅行(中等)(LCS方案数打印)

在这里插入图片描述
是最长公共子序列的样子,但是需要输出可能方案情况,这就不好办了。

import java.util.*;
import java.io.*;

public class Main {
    static BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
    static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    static String a, b;
    static int[][] dp;
    static int n1, n2;
    static int[][] lst1;
    static int[][] lst2;
    // 存储最后可能路径
    static List<String> list = new LinkedList<>();
    public static void main(String[] args) throws IOException {
        a = reader.readLine().trim();
        b = reader.readLine().trim();
        // dp[i][j]
        // a[1...i] 与 b[1...j] 的 LCS
        n1 = a.length();
        n2 = b.length();

        // lst[i][j],记录串中前i个字符中,第j个字母(0-25号字母)最后一次出现的位置
        lst1 = new int[n1 + 1][30];
        lst2 = new int[n2 + 1][30];
        for (int i = 1; i <= n1; i++) {
            for (int j = 0; j < 26; j++) {
                if (a.charAt(i - 1) == (j + 'a')) {
                    lst1[i][j] = i;
                } else {
                    // 如果不相等,只能看前i-1个字符
                    lst1[i][j] = lst1[i - 1][j];
                }
            }
        }
        for (int i = 1; i <= n2; i++) {
            for (int j = 0; j < 26; j++) {
                if (b.charAt(i - 1) == (j + 'a')) {
                    lst2[i][j] = i;
                } else {
                    lst2[i][j] = lst2[i - 1][j];
                }
            }
        }
        dp = new int[n1 + 1][n2 + 1];
        for (int i = 1; i <= n1; i++) {
            for (int j = 1; j <= n2; j++) {
                if (a.charAt(i - 1) == b.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        dfs(n1, n2, new StringBuilder());
        // 升序输出所有结果(排序 + 去重)
        Collections.sort(list);
        for (int i = 0; i < list.size(); i++) {
            if (i > 0 && list.get(i - 1).equals(list.get(i))) continue;
            System.out.println(list.get(i));
        }
    }
    static void dfs(int n, int m, StringBuilder sb) {
        if (dp[n][m] + sb.length() < dp[n1][n2]) return;

        // 遍历完,存入最终结果
        if (n == 0 || m == 0) {
            list.add(sb.toString());
            return;
        }
        // 最后一个字符相同时才考虑下一个字符
        if (dp[n - 1][m - 1] + 1 == dp[n][m] && a.charAt(n - 1) == b.charAt(m - 1)) {
            dfs(n - 1, m - 1, new StringBuilder(sb).insert(0, a.charAt(n - 1)));
        } else {
            // 26个字母,暴力枚举两个串中最后一个可能相同的字符
            // dfs函数中的第一行就对不满足题意的情况进行了剪枝
            for (int i = 0; i < 26; i++) {
                int tx = lst1[n][i];
                int ty = lst2[m][i];
                dfs(tx, ty, sb);
            }
        }
    }
}

※🥠E 子串(中等)

在这里插入图片描述
在这里插入图片描述
公共序列的感觉,但是由于要去取出k个互不重叠的非空子串(一个字符也算子串),来拼接出新的子串,这就很…。
题解都没看懂,g 学完了又来了

首先分析题目中需要考虑的状态,1:A的长度,2:B的长度,3:取K个子串,所以可以得到dp数组的定义,dp[i][j][k],A串中前 i 个字符取 k 个子串,与B串中前 j 个 字符匹配的方案数。显然dp[0…n][0][0] = 1,不管A的长度是多少,不选任何子串,B也不匹配任何字符(也就是空串),只有一种方案:空串!


考虑,状态转移方程?

能够确定的就是A与B的字符相等,当A[i] == B[j] 时,可以不选择A的这个字符,那么,dp[i][j][k] = dp[i -1][j][k],就是还得考虑剩下的字符中:选择k个子串的方案数。

如果选择呢?
由于我们这里只规定了需要选出k个子串,但没有规定子串的长度大小,所以我们需要考虑k的大小? 假设这里取该子串的长度为t,那么dp[i][j][k] = dp[i-t][j-t][k-1],t的大小是可以从1 - m 的,所以这里需要对 dp[i-t][j-t][k-1] 中的 t 遍历求和。

所以,如果A[i] == B[j],dp[i][j][k] = dp[i-1][j][k] + sum(dp[i-t][j-t][k-1])
A[i] != B[j],怎么找?

很简单,说明当前字符不能拿,我们要的是匹配B串的方案数,B串肯定是不能变的,所以,直接继承dp[i-1][j][k]即可。

我们可以令sum[i][j][k] = sum(dp[i-t][j-t][k -1], t从1开始),sum[i][j][k] = dp[i-1][j-1][k-1] + dp[i-2][j-2][k-1] + … + dp[i-t][j-t][k-1],那么sum[i][j][k] = sum[i - 1][j - 1][k] + dp[i-1][j-1][k-1] (当然也可以令sum[i][j][k-1] = sum(dp[i-t][j-t][k-1]),只是用于记录累加和)

如果A[i] == B[j],就按照上面的sum进行求和
如果A[i] != B[j],说明当前A的字符无法匹配,只能继承dp[i-1][j][k],那么让sum=0即可。

处理之后,dp[i][j][k] = dp[i-1][j][k] (可以用但不选择) + sum[i][j][k] (选择)

三维dp数组空间超额,考虑到 i 状态只使用了上下两层的状态,所以可以进行空间压缩,

import java.util.*;
import java.io.*;

public class Main {
    static BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
    static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    public static void main(String[] args) throws IOException {
        String[] input = reader.readLine().trim().split(" ");
        int n = Integer.parseInt(input[0]);
        int m = Integer.parseInt(input[1]);
        int k = Integer.parseInt(input[2]);

        String a = reader.readLine().trim();
        String b = reader.readLine().trim();
        // dp[i][j][k] a的前i个字符中,选出k个子串,与B的前j个字符匹配的方案数
        // i可以做空间压缩
        int[][] dp = new int[m + 1][k + 1];
        dp[0][0] = 1;  // 空串 + 不选,方案数=1

        int MOD = (int)1e9 + 7;

        // 辅助进行dp运算
        int[][] sum = new int[m + 1][k + 1];
        for (int i = 1; i <= n; i++) {
            // 考虑到转移方程的性质,进行空间压缩时需要逆序遍历j、k
            for (int j = m; j >= 1; j--) {
                for (int kk = k; kk >= 1; kk--) {
                    if (a.charAt(i - 1) == b.charAt(j - 1)) {
                        sum[j][kk] = (sum[j - 1][kk] + dp[j - 1][kk - 1]) % MOD;
                    } else {
                        // 不相等就无法进行匹配,方案数=0
                        sum[j][kk] = 0;
                    }
                    dp[j][kk] = (dp[j][kk] + sum[j][kk]) % MOD;
                }
            }
        }
        System.out.println(dp[m][k]);
    }
}

🍭 DP数组压缩问题

一个老生常谈的问题,这里以0-1背包问题为例进行讲解:

for(int i = 1; i <= n; i++) 
    for(int j = 1; j <= m; j++)
    {
        //  当前背包容量装不进第i个物品,则价值等于前i-1个物品
        if(j < v[i]) 
            f[i][j] = f[i - 1][j];
        // 能装,需进行决策是否选择第i个物品
        else    
            f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
    }  

从上面代码中可以发现,f[i][j] 的状态,只与 f[i-1][j],f[i-1][j-v[i]] 有关,也就是说真正用到的只是i 和 i-1,上下两层,没有必要为 i 再开一层数组,但是怎么压缩呢?

考虑到,f[i][j],是从 f[i - 1][j - v[i]] 转移过来,我们要避免更新 f[i][j]时,f[i-1][j-v[i] 的值被更新,怎么做呢?那就可以逆序遍历 j,这样从大到小的去枚举 j,更新 f[i][j] 时,由于还没有更新到 f[i-1][j - v[i]],所以此时 f[j - v[i]] 的值还是上一层 f[i-1][j-v[i]] 的值,这样就实现了dp数组的压缩。

是否能够压缩,要看转移方程是否只跟上下两层的状态有关,以及其它状态是依靠之前、还是之后的状态转移而来,来确定是否需要将状态的枚举进行逆序。

for(int i = 1; i <= n; i++)
{
    for(int j = m; j >= v[i]; j--)  
        f[j] = max(f[j], f[j - v[i]] + w[i]);
} 

做dp题目,一定要想到,之前的什么状态可以转换到当前状态,而不是当前状态可以转换到什么状态,思维是逆向的

🎂F 乌龟棋(中等)

在这里插入图片描述
在这里插入图片描述
首先抓住题目中的关键信息:

  • 1、只有四种卡片
  • 2、每张卡片只能用一次
  • 3、每种卡片的数量不会超过40张
  • 4、题目的输入保证到达终点时用完所有的卡片

1和2的条件合并,可以得出:每种卡片只能用k次,k就是输入中每种卡片的出现次数。

由于我们的移动距离 和 获得的分数完全取决于卡片的选取,所以我们可以用4种卡片作为状态,dp[i,j,k,l],i、j、k、l就分别对应四种卡片的选取数量,最后的答案就是dp[s1,s2,s3,s4],也就是上面说的第四点,到达终点时,所有卡片都用完了。

我们可以通过当前选择的每种卡片的数量,来确定当前所在位置,并且每次只能选择四种卡片中的一种卡片中的一张,当然该种类的卡片数量一定要 > 0。

import java.util.*;
import java.io.*;

public class Main {
    static BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
    static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    public static void main(String[] args) throws IOException {
        String[] input = reader.readLine().trim().split(" ");
        int n = Integer.parseInt(input[0]);
        int m = Integer.parseInt(input[1]);
        input = reader.readLine().trim().split(" ");
        int[] score = new int[n + 1];
        for (int i = 0; i < n; i++) {
            score[i + 1] = Integer.parseInt(input[i]);
        }
        input = reader.readLine().trim().split(" ");
        // 统计每种卡片的张数
        int[] card = new int[5];
        for (int i = 0; i < m; i++) {
            card[Integer.parseInt(input[i])]++;
        }
        // 四种卡片的张数作为状态
        int[][][][] dp = new int[card[1] + 1][card[2] + 1][card[3] + 1][card[4] + 1];
        // 一张卡片都不选,并且必须从起点开始
        dp[0][0][0][0] = score[1];
        for (int i = 0; i <= card[1]; i++) {
            for (int j = 0; j <= card[2]; j++) {
                for (int k = 0; k <= card[3]; k++) {
                    for (int l = 0; l <= card[4]; l++) {
                        if (i == 0 && j == 0 && k == 0 && l == 0) continue;
                        int maxx = 0;
                        // 当前所在位置(根据起点 + 所选取的卡片总和)
                        int now = 1 + i + 2 * j + 3 * k + 4 * l;
                        // 每次只能选择一张卡片,所以需要确定选择四种卡片中的哪一张
                        if (i != 0) maxx = Math.max(maxx, dp[i - 1][j][k][l] + score[now]);
                        if (j != 0) maxx = Math.max(maxx, dp[i][j - 1][k][l] + score[now]);
                        if (k != 0) maxx = Math.max(maxx, dp[i][j][k - 1][l] + score[now]);
                        if (l != 0) maxx = Math.max(maxx, dp[i][j][k][l - 1] + score[now]);
                        // 得到当前卡片选取情况的最大值
                        dp[i][j][k][l] = maxx;
                    }
                }
            }
        }
        // 题目保证到达终点时,一定使用完所有的卡片,也就告诉了答案
        System.out.println(dp[card[1]][card[2]][card[3]][card[4]]);
    }
}

🍝G 饼干(中等)

在这里插入图片描述
怨气总和 = g[1] * a[1] + g[2] * a[2] + … + g[i] * a[i],我们把每个孩子按照g值,从大到小排序,那么根据排序不等式(看下面的排队打水问题),我们要让排序后的g,从大到小所匹配的a要从小到大,也就是说让g值大的分配的a值要小,怎么才能让a值小呢?很简单,给他分配很多很多饼干,所以得到了最终的对应关系:
g值越大,分配的饼干越多(分配的饼干数是随着g值单减而单减)(这样才能使得其匹配的a值越小,a值代表比当前孩子饼干数多的孩子数)

dp[i,j]:前 i 个小朋友分配 j 块饼干的方案的集合(也就是所有分配的可能方案),关键在于状态的转移,题目中特殊的点在于,每个小朋友至少要有一块饼干,那么,我们可以以此为依据,枚举前 i 个小朋友中有几个小朋友分配的饼干数 = 1(这个划分方式类似于下面的:整数划分 => 以正整数1作为划分依据),那么:
前 i 个小朋友中,分配的饼干数=1的小朋友数,可以为0、1、2、3...i。

假设有k个小朋友分配的饼干数=1,此时dp[i][j] = dp[i - k][j - k] (前i-k个小朋友,分配j-k个饼干) + (g[i - k + 1] + … +g[i]) * (i - k),后面加的这部分就是这k个小朋友产生的怨气总和(前面的累加可以用前缀和处理)

特殊情况:k = 0,也就是说没有饼干数分配为1的小朋友,我们可以从每个小朋友处拿走一块饼干,所以dp[i][j] = dp[i][j - i]

最终,使得怨气总和最小的分配方案就出现在上面的集合中(看有几个小朋友分配的饼干数=1)

import java.util.*;
import java.io.*;

public class Main {
    static BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
    static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    public static void main(String[] args) throws IOException {
        String[] input = reader.readLine().trim().split(" ");
        int n = Integer.parseInt(input[0]);
        int m = Integer.parseInt(input[1]);
        // n个孩子,m块饼干
        input = reader.readLine().trim().split(" ");
        int[] g = new int[n + 1];

        for (int i = 0; i < n; i++) {
            g[i + 1] = Integer.parseInt(input[i]);
        }
        // 将怨气值从大到小进行排序
        Arrays.sort(g, 1, 1 + n);
        // 逆序
        for (int i = 1; i <= n/2; i++) {
            int tmp = g[i];
            g[i] = g[n - i + 1];
            g[n - i + 1] = tmp;
        }
        // 逆序完之后再求前缀和
        // 怨气值的前缀和
        int[] preSum = new int[n + 1];
        for (int i = 1; i <= n; i++) {
        	// 注意这里的前缀和和原数组下标都是从1开始
            preSum[i] = preSum[i - 1] + g[i];
        }
        // dp[i][j],前i个孩子,分配j个饼干的最小怨气总和
        int[][] dp = new int[n + 1][m + 1];
        for (int i = 0; i < n + 1; i ++) {
            Arrays.fill(dp[i], 0x3f3f3f3f);
        }
        // base case
        dp[0][0] = 0;
        for (int i = 1; i <= n; i++) {  // 遍历孩子数i
            for (int j = 1; j <= m; j++) {  // 遍历分配饼干数j
                if (j < i) continue;  // 饼干数小于孩子数,不满足题意(每个孩子至少一块饼干)
                // 先考虑没有饼干数=1的孩子数
                if (j >= i) dp[i][j] = dp[i][j - i];
                for (int k = 1; k <= i && k <= j; k++) {  // 遍历分配饼干数=1的孩子数
                    dp[i][j] = Math.min(dp[i][j], dp[i - k][j - k] + (preSum[i] - preSum[i - k]) * (i - k));
                }
            }
        }
        System.out.println(dp[n][m]);
    }
}

🍶补充题目:整数划分

注意状态的划分依据:以整数1作为划分
在这里插入图片描述
因为要把一个正整数n划分成若干个正整数,可以把这个n当作背包的容量,若干个正整数当作物品,就相当于求物品恰好装满背包容量的方案数,这若干个正整数的大小应该<=n。

dp[i][j] 表示,用[1…i]的正整数,表示出正整数 j 的方案数,显然dp[i][0] = 1,要组合出0(虽然0不是正整数,但它作为base case用于其它状态的转移),只需一个数都不选即可,那么至少都有一种方案数。

import java.util.*;
import java.io.*;

public class Main {
    static BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
    static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    public static void main(String[] args) throws IOException {
        int n = Integer.parseInt(reader.readLine().trim());
        // dp[i][j]:[1...i] 能够组合出 j 的方案数,显然i最大=j
        int[][] dp = new int[n + 1][n + 1];  // 答案:dp[n][n]
        // 只要j=0,不论i取多少(i>=1),都有至少一种方案:都不拿
        for (int i = 1; i <= n; i++) {
            dp[i][0] = 1;
        }
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                // 首先可以不使用当前的j(不选用当前物品)
                dp[i][j] = dp[i - 1][j];
                // 背包容量 >= 物品的体积
                if (j >= i) {
                    // 由于不限制每个数字使用的次数,所以可以继续考虑dp[i][j - i]
                    dp[i][j] = dp[i - 1][j] + dp[i][j - i];
                }
            }
        }
        System.out.println(dp[n][n]);
    }
}

🥩补充题目:排队打水问题

属于排序不等式的模板题,本质是贪心
在这里插入图片描述
在这里插入图片描述

假设排队的顺序为:1 2 3 4 . . . n,对应单独一人装满水所需时间为:t1 t2 t3 t4 … tn,总的等待时间 = t1 * (n-1) + t2 * (n-2) + t3 * (n-3) + ... + tn * 0,由于排在前面的人的系数最大,所以应该按照每个人单独装满水的所需时间从小到大排序,所需时间越久就越排在后面。

import java.util.*;
import java.io.*;

public class Main {
    static BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
    static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    public static void main(String[] args) throws IOException {
        int n = Integer.parseInt(reader.readLine().trim());
        int[] time = new int[n + 1];
        String[] input = reader.readLine().trim().split(" ");
        for (int i = 0; i < n; i++) {
            time[i + 1] = Integer.parseInt(input[i]);
        }
        // 按照时间从小到大排序
        Arrays.sort(time, 1, n + 1);
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += time[i] * (n - i);
        }
        System.out.println(sum);
    }
}

※🌯H 分级(中等)

在这里插入图片描述
注意题目中没有对B中元素做任何限制,但存在这么一个数学性质:
性质:一定存在一组最优解B[i],使得每一个B[i]都在A数组中出现过

现在,问题转换为,从A数组中选N个数(只是选N个数,可以重复)构成B数组(B数组非严格单调),使得表达式S最小。

在这里插入图片描述
在这里插入图片描述
注意将f[i][j]所代表的集合划分成 j 个不重不漏的子集时,不需要再开一层循环来遍历A’[i],由于B数组是非严格单调,所以A’[i]可以取到A’[j],只要在 <= j即可(无论非严格递增、非严格递减都可以)。

import java.util.*;
import java.io.*;

public class Main {
    static BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
    static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    public static void main(String[] args) throws IOException {
        int n = Integer.parseInt(reader.readLine().trim());
        int[] a = new int[n + 1];
        int[] b = new int[n + 1];
        for (int i = 1; i <= n; i++) {
            a[i] = Integer.parseInt(reader.readLine().trim());
            b[i] = a[i];
        }
        // 先考虑非单调递增的情况
        Arrays.sort(b, 1, n + 1);
        // dp[i][j]构造好了A[1..i]对应的B[1..i]且B数组的最后一个数=A'[j]的最小S值
        int[][] dp = new int[n + 1][n + 1];
        for (int i = 1; i <= n; i++) {
            int min = 0x3f3f3f3f;
            for (int j = 1; j <= n; j++) {
                // 考虑倒数第二个数的选择情况:只能选择[1...j]的下标
                min = Math.min(min, dp[i - 1][j]);
                dp[i][j] = min + Math.abs(b[j] - a[i]);
                // A B数组的最后一个元素下标=i
                // 且B数组的最后一个元素B[i] = A'[j] = b[j]
            }
            // 对于每一个dp[n][j],都对应着以A'[j]结尾的B数组的最小S值
            // 但还需要知道全局的最小S值,所以最后要遍历所有可能的A'[j]
        }
        int ans = 0x3f3f3f3f;
        for (int i = 1; i <= n; i++) {
            ans = Math.min(ans, dp[n][i]);
        }

        // 还要考虑非单调递减的情况(和上面方法一样,只是把A'数组顺序改一下)
        // 逆序
        for (int i = 1; i <= n / 2; i++) {
            int cur = b[i];
            b[i] = b[n - i + 1];
            b[n - i + 1] = cur;
        }
        dp = new int[n + 1][n + 1];
        for (int i = 1; i <= n; i++) {
            int min = 0x3f3f3f3f;
            for (int j = 1; j <= n; j++) {
                min = Math.min(min, dp[i - 1][j]);
                dp[i][j] = min + Math.abs(b[j] - a[i]);
            }
        }
        // 求全局最小值
        for (int i = 1; i <= n; i++) {
            ans = Math.min(ans, dp[n][i]);
        }
        System.out.println(ans);
    }
}

🌮补充题目:最长公共子序列

在这里插入图片描述
经典板子题,遍历两个字符串中的字符,相等时,dp[i][j] = dp[i - 1][j - 1],不等时,看是删掉第一个字符串中的字符,还是删掉第二个字符串中的字符(同时删除两个字符的情况包含在上面两个情况中,所以不需要考虑)。

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int n1 = text1.length();
        int n2 = text2.length();
        int[][] dp = new int[n1 + 1][n2 + 1];
        // dp[i][j] s1[1...i] 与 s2[1...j]的最长公共子序列长度
        for (int i = 1; i <= n1; i++) {
            for (int j = 1; j <= n2; j++) {
                if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    // 只能继承其中一个
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[n1][n2];
    }
}

🍯补充题目:最长递增子序列

在这里插入图片描述
由于递增子序列的集合中,我们可以确定的是最后一个元素,确定了最后一个元素后,我们还需要确定最后一个元素的前一个元素是谁,也就是子序列的倒数第二个元素,仔细想想,其实这和上面的题目“分级”是一样的道理,最后一个元素确定,在此基础上考虑倒数第二个不同的元素,最后的全局最大值,就是需要遍历:最后一个元素。

子集的划分依据:最后一个元素 + 倒数第二个元素

class Solution {
    public int lengthOfLIS(int[] nums) {
        int n = nums.length;
        int[] dp = new int[n];
        // dp[i] 以第i个数结尾的最长递增子序列的长度
        int ans = 0xc0c0c0c0;
        for (int i = 0; i < n; i++) {
            dp[i] = 1;
            for (int j = 0; j < i; j++) {
                // 考虑倒数第二个元素的值
                if (nums[j] < nums[i])
                    dp[i] = Math.max(dp[i], dp[j] + 1);
            }
            ans = Math.max(dp[i], ans);
        }
        return ans;
    }
}

🦪补充题目:最长公共上升子序列

在这里插入图片描述
在这里插入图片描述
关键是状态的表示,结合了上面几道题的状态表示方法,需要理解。

关键:以倒数第二个元素为划分要点,对集合进行划分。

未优化版本:

import java.util.*;
import java.io.*;

public class Main {
    static BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
    static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    public static void main(String[] args) throws IOException {
        int n = Integer.parseInt(reader.readLine().trim());
        int[] a = new int[n + 1];
        String[] input = reader.readLine().trim().split(" ");
        for (int i = 0; i < n; i++) {
            a[i + 1] = Integer.parseInt(input[i]);
        }
        input = reader.readLine().trim().split(" ");
        int[] b = new int[n + 1];
        for (int i = 0; i < n; i++) {
            b[i + 1] = Integer.parseInt(input[i]);
        }
        int[][] dp = new int[n + 1][n + 1];
        // dp[i][j] a[1...i] 与 b[1...j] 且 以b[j]结尾的 最长公共递增子序列的长度
        // 公共 且 严格递增
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                // 不包含a[i]
                dp[i][j] = dp[i - 1][j];
                if (a[i] == b[j]) {
                    int max = 1;
                    // 枚举所有可能的倒数第二个元素的值
                    for (int k = 1; k < j; k++) {
                        // 显然倒数第二个元素的下标idx必须<j
                        if (a[i] > b[k]) {
                            // 能够作为倒数第二个元素的前提是比倒数第一个元素的值小(严格递增)
                            max = Math.max(max, dp[i - 1][k] + 1);  // +1是因为倒数第一个元素满足
                        }
                    }
                    dp[i][j] = Math.max(dp[i][j], max);
                }
            }
        }
        int ans = 0;
        for (int i = 1; i <= n; i++) ans = Math.max(ans, dp[n][i]);
        System.out.println(ans);
    }
}

发现下面的第三个for循环是可以优化的:
在这里插入图片描述
只需要在第二个for循环中记录这个max值即可,这个max值就可以覆盖第三个for循环中的max值。

import java.util.*;
import java.io.*;

public class Main {
    static BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
    static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    public static void main(String[] args) throws IOException {
        int n = Integer.parseInt(reader.readLine().trim());
        int[] a = new int[n + 1];
        String[] input = reader.readLine().trim().split(" ");
        for (int i = 0; i < n; i++) {
            a[i + 1] = Integer.parseInt(input[i]);
        }
        input = reader.readLine().trim().split(" ");
        int[] b = new int[n + 1];
        for (int i = 0; i < n; i++) {
            b[i + 1] = Integer.parseInt(input[i]);
        }
        int[][] dp = new int[n + 1][n + 1];
        // dp[i][j] a[1...i] 与 b[1...j] 且 以b[j]结尾的 最长公共递增子序列的长度
        // 公共 且 严格递增
        for (int i = 1; i <= n; i++) {
            int max = 1;
            for (int j = 1; j <= n; j++) {
                // 不包含a[i]
                dp[i][j] = dp[i - 1][j];
                if (a[i] > b[j]) {
                    max = Math.max(max, dp[i - 1][j] + 1);
                }
                if (a[i] == b[j]) {
                    dp[i][j] = Math.max(dp[i][j], max);
                }
            }
        }
        int ans = 0;
        for (int i = 1; i <= n; i++) ans = Math.max(ans, dp[n][i]);
        System.out.println(ans);
    }
}

※🥐I 关路灯(困难)

在这里插入图片描述

在这里插入图片描述

整个推导非常的复杂,可以去看洛谷的题解:https://www.luogu.com.cn/blog/user44468/solution-p1220

这里就自己做完之后的感想:
把关闭路灯当作:区间问题来对待,最终的目的是为了关闭整个区间内的所有路灯。站在某一位置,下一步可以朝着目前的方向继续往下走,也可以转过身朝着相反的方向走,这就需要考虑上一次关闭区间的路灯后,站在区间的左端点还是右端点,当然也需要考虑关闭当前区间的灯后,站在左端点还是右端点。

对于当前区间[i,j],假设最后站在端点 i,那么它可以从上一区间的左端点继续走得到,也可以从上一区间的右端点反着走到端点 i 得到。
对于当前区间[i,j],假设最后站在端点 j,那么它可以从上一区间的右端点继续走得到,也可以从上一区间的左端点反着走到端点 j 得到。

import java.util.*;
import java.io.*;

public class Main {
    static BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
    static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    public static void main(String[] args) throws IOException {
        String[] input = reader.readLine().trim().split(" ");
        int n = Integer.parseInt(input[0]);
        int c = Integer.parseInt(input[1]);
        int[] p = new int[n + 1];
        int[] w = new int[n + 1];
        // 记录功率前缀和
        int[] preSum = new int[n + 1];
        for (int i = 0; i < n; i++) {
            input = reader.readLine().trim().split(" ");
            p[i + 1] = Integer.parseInt(input[0]);
            w[i + 1] = Integer.parseInt(input[1]);
            preSum[i + 1] = preSum[i] + w[i + 1];
        }
        // dp[i][j][0/1]
        // 关闭区间i,j的路灯所需的最小耗电量
        int[][][] dp = new int[n + 1][n + 1][2];
        for (int i = 0; i < n + 1; i++) {
            for (int j = 0; j < n + 1; j++) {
                Arrays.fill(dp[i][j], 0x3f3f3f3f);
            }
        }
        // 刚开始所站的位置可以直接关掉,不需要耗电
        dp[c][c][0] = 0;
        dp[c][c][1] = 0;
        // 区间dp,先枚举区间长度,再枚举端点
        // 先遍历区间长度
        for (int l = 2; l <= n; l++) {
            // 再枚举左端点,注意右端点不能超出n
            for (int i = 1; i + l - 1 <= n; i++) {
            	// 右端点的下标
                int j = i + l - 1;
                // 最后站在端点i的位置
                // 可以从上一区间的i端点到i, 也可以从上一区间的j端点到i
                // 此时i + 1、j已经被关了
                dp[i][j][0] = Math.min(dp[i + 1][j][0] + (p[i + 1] - p[i]) * (preSum[n] - preSum[j] + preSum[i]),
                        dp[i + 1][j][1] + (p[j] - p[i]) * (preSum[n] - preSum[j] + preSum[i]));
                // 最后站在端点j的位置
                // 可以从上一区间的j端点到j,也可以从上一区间的i端点到j
                // 此时i、j-1已经被关了
                dp[i][j][1] = Math.min(dp[i][j - 1][0] + (p[j] - p[i]) * (preSum[n] - preSum[j - 1] + preSum[i - 1]),
                        dp[i][j - 1][1] + (p[j] - p[j - 1]) * (preSum[n] - preSum[j - 1] + preSum[i - 1]));
            }
        }
        // 关闭1-n,最后可以站在左端点,也可以站在右端点
        int ans = Math.min(dp[1][n][0], dp[1][n][1]);
        System.out.println(ans);
    }
}

🍞J 最大上升子序列和(困难)

在这里插入图片描述

在这里插入图片描述
看了下题解:dp + 树状数组 + 离散化…
人麻了人麻了,看不懂啊,上一题已经是极限了

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@u@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值