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);
}
}
站在当前地窖考虑,考虑前面有几个地窖能到达当前地窖,所以可以很容易得到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);
}
}
首先需要知道的是,直接走和使用技能哪个更快:
我们可以先算出来只使用技能的情况下,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]);
}
}
}
是最长公共子序列的样子,但是需要输出可能方案情况,这就不好办了。
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);
}
}
}
}
公共序列的感觉,但是由于要去取出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]);
}
}
一个老生常谈的问题,这里以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题目,一定要想到,之前的什么状态可以转换到当前状态,而不是当前状态可以转换到什么状态,思维是逆向的
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[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);
}
}
注意题目中没有对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必须
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);
}
}
整个推导非常的复杂,可以去看洛谷的题解: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);
}
}