DP:最长上升子序列模型
现在打算写一些短点的文章了,LeetCode 系列不会再 append 了,如果写 lc 题会单独开一篇文章,然后写题解
最长上升子序列模型
LIS 有 N^2 的 DP 解法,也有 NlogN 的贪心的解法,具体用那种取决于数据范围
1017. 怪盗基德的滑翔翼
怪盗基德是一个充满传奇色彩的怪盗,专门以珠宝为目标的超级盗窃犯。
而他最为突出的地方,就是他每次都能逃脱中村警部的重重围堵,而这也很大程度上是多亏了他随身携带的便于操作的滑翔翼。
有一天,怪盗基德像往常一样偷走了一颗珍贵的钻石,不料却被柯南小朋友识破了伪装,而他的滑翔翼的动力装置也被柯南踢出的足球破坏了。
不得已,怪盗基德只能操作受损的滑翔翼逃脱。
假设城市中一共有 N 幢建筑排成一条线,每幢建筑的高度各不相同。
初始时,怪盗基德可以在任何一幢建筑的顶端。
他可以选择一个方向逃跑,但是不能中途改变方向(因为中森警部会在后面追击)。
因为滑翔翼动力装置受损,他只能往下滑行(即:只能从较高的建筑滑翔到较低的建筑)。
他希望尽可能多地经过不同建筑的顶部,这样可以减缓下降时的冲击力,减少受伤的可能性。
请问,他最多可以经过多少幢不同建筑的顶部(包含初始时的建筑)?
输入格式
输入数据第一行是一个整数 K,代表有 K 组测试数据。
每组测试数据包含两行:第一行是一个整数 N,代表有 N 幢建筑。第二行包含 N 个不同的整数,每一个对应一幢建筑的高度 h,按照建筑的排列顺序给出。
输出格式
对于每一组测试数据,输出一行,包含一个整数,代表怪盗基德最多可以经过的建筑数量。
数据范围
1≤K≤100, 1≤N≤100, 0<h<10000
输入样例:
3
8
300 207 155 299 298 170 158 65
8
65 158 170 298 299 155 207 300
10
2 1 3 4 5 6 7 8 9 10
输出样例:
6
9
解法一
常规 N^2 动态规划的方式 & NlogN 贪心解法(NlogN 的解法回忆了一会儿才推出来,如果数据范围不是很大还是 N^2 的 dp 好写)
import java.io.*;
import java.util.*;
//AcWing 1017. 怪盗基德的滑翔翼 https://www.acwing.com/problem/content/1019/
class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int T = sc.nextInt();
while (T-- > 0) {
int n = sc.nextInt();
int[] w = new int[n];
for (int i = 0; i < n; i++) {
w[i] = sc.nextInt();
}
System.out.println(Math.max(solve(w, n), solve(reverse(w), n)));
}
}
//DP(N^2) 的解法
public static int solve(int[] w, int n) {
int[] dp = new int[n];
Arrays.fill(dp, 1);
int res = 1;
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) {
if (w[j] > w[i]) {
dp[i] = Math.max(dp[j]+1, dp[i]);
}
}
res = Math.max(res, dp[i]);
}
return res;
}
//tail[i]: 长度为 i 的递减子序列最大结尾
public static int solve2(int[] w, int n) {
int[] tail = new int[n];
int len = 0;
for (int i = 0; i < n; i++) {
int idx = search(tail, len, w[i]);
if (idx == -1) {
tail[len++] = w[i];
} else {
tail[idx] = w[i];
}
}
return len;
}
//从左到右找第一个小于 target 的元素下标,替换它,使得结尾更大,长度更长
public static int search(int[] tail, int len, int target){
int left = 0, right = len-1;
int res = -1;
while (left <= right) {
int mid = left + (right - left)/2;
if (tail[mid] <= target) {
res = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
return res;
}
public static int[] reverse(int[] w) {
for (int i = 0, j = w.length-1; i < j; i++, j--) {
int temp = w[i];
w[i] = w[j];
w[j] = temp;
}
return w;
}
}
1014. 登山
五一到了,ACM 队组织大家去登山观光,队员们发现山上一个有 N 个景点,并且决定按照顺序来浏览这些景点,即每次所浏览景点的编号都要大于前一个浏览景点的编号。
同时队员们还有另一个登山习惯,就是不连续浏览海拔相同的两个景点,并且一旦开始下山,就不再向上走了。
队员们希望在满足上面条件的同时,尽可能多的浏览景点,你能帮他们找出最多可能浏览的景点数么?
输入格式
第一行包含整数 N,表示景点数量。
第二行包含 N 个整数,表示每个景点的海拔。
输出格式
输出一个整数,表示最多能浏览的景点数。
数据范围 2≤N≤1000 输入样例:
8
186 186 150 200 160 130 197 220
输出样例:
4
解法一
LIS 转换了一下而已,求每个点从左到右 和 从右到左的最长上升子序列最大和就 ok 了
import java.util.*;
import java.io.*;
class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int N = sc.nextInt();
int[] w = new int[N];
for (int i = 0; i < N; i++) {
w[i] = sc.nextInt();
}
System.out.println(solve(w, N));
}
//186 186 150 200 160 130 197 220 := 4
public static int solve(int[] w, int N) {
int[] dp1 = new int[N];
int[] dp2 = new int[N];
Arrays.fill(dp1, 1);
Arrays.fill(dp2, 1);
int res = 1;
for (int i = 0; i < N; i++) {
for (int j = 0; j < i; j++) {
if (w[j] < w[i]) {
dp1[i] = Math.max(dp1[j]+1, dp1[i]);
}
}
}
for (int i = N-1; i >= 0; i--) {
for (int j = N-1; j > i; j--) {
if (w[j] < w[i]) {
dp2[i] = Math.max(dp2[j]+1, dp2[i]);
}
}
res = Math.max(res, dp1[i]+dp2[i]-1);
}
return res;
}
}
482. 合唱队形 和这一题一样,不重复写了
1012. 友好城市
Palmia 国有一条横贯东西的大河,河有笔直的南北两岸,岸上各有位置各不相同的 N 个城市。
北岸的每个城市有且仅有一个友好城市在南岸,而且不同城市的友好城市不相同。
每对友好城市都向政府申请在河上开辟一条直线航道连接两个城市,但是由于河上雾太大,政府决定避免任意两条航道交叉,以避免事故。
编程帮助政府做出一些批准和拒绝申请的决定,使得在保证任意两条航线不相交的情况下,被批准的申请尽量多。
输入格式
第 1 行,一个整数 N,表示城市数。
第 2 行到第 n+1 行,每行两个整数,中间用 1 个空格隔开,分别表示南岸和北岸的一对友好城市的坐标。
输出格式
仅一行,输出一个整数,表示政府所能批准的最多申请数。
数据范围
1≤N≤5000, 0≤xi≤10000
输入样例:
7
22 4
2 6
10 3
15 12
9 8
17 17
4 2
输出样例:
4
解法一
和之前的 354. 俄罗斯套娃信封问题 一样,还简单一点,坐标都是唯一的,不需要考虑坐标重合的问题,具体可以去看看俄罗斯套娃的这个题
import java.util.*;
import java.io.*;
class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int N = sc.nextInt();
int[][] w = new int[N][2];
for (int i = 0; i < N; i++) {
w[i][0] = sc.nextInt();
w[i][1] = sc.nextInt();
}
System.out.println(solve2(w, N));
}
//5000 * 5000 = 2500 0000,能过但是有点慢
public static int solve(int[][] w, int N) {
Arrays.sort(w, (w1, w2)->w1[0]-w2[0]);
int res = 1;
int[] dp = new int[N];
for (int i = 0; i < N; i++) {
dp[i] = 1;
for (int j = 0; j < i; j++) {
if (w[j][0] < w[i][0] && w[j][1] < w[i][1]) {
dp[i] = Math.max(dp[j]+1, dp[i]);
}
}
res = Math.max(res, dp[i]);
}
return res;
}
//NlogN 贪心二分的做法
public static int solve2(int[][] w, int N) {
Arrays.sort(w, (w1, w2)->w1[0]-w2[0]);
int[] tail = new int[N];
int len = 0;
for (int i = 0; i < N; i++) {
int idx = search(tail, len, w[i][1]);
if (idx == -1) {
tail[len++] = w[i][1];
} else {
tail[idx] = w[i][1];
}
}
return len;
}
//从左到右找第一个大于 target 的,后面替换它,使得结尾更小
public static int search(int[] tail, int len, int target) {
int left = 0, right = len-1;
int res = -1;
while (left <= right) {
int mid = left + (right - left)/2;
if (tail[mid] > target) {
res = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
return res;
}
}
1016. 最大上升子序列和
一个数的序列 bi,当 b1<b2<…<bS 的时候,我们称这个序列是上升的。
对于给定的一个序列 (a1,a2,…,aN),我们可以得到一些上升的子序列 (ai1,ai2,…,aiK),这里 1≤i1<i2<…<iK≤N。
比如,对于序列 (1,7,3,5,9,4,8),有它的一些上升子序列,如 (1,7),(3,4,8) 等等。
这些子序列中和最大为 18,为子序列 (1,3,5,9) 的和。
你的任务,就是对于给定的序列,求出最大上升子序列和。
注意,最长的上升子序列的和不一定是最大的,比如序列 (100,1,2,3) 的最大上升子序列和为 100,而最长上升子序列为 (1,2,3)。
输入格式
输入的第一行是序列的长度 N。
第二行给出序列中的 N 个整数,这些整数的取值范围都在 0 到 10000(可能重复)。
输出格式
输出一个整数,表示最大上升子序列和。
数据范围
1≤N≤1000
输入样例:
7
1 7 3 5 9 4 8
输出样例:
18
解法一
没啥好说的,比较简单
import java.util.*;
import java.io.*;
class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int N = sc.nextInt();
int[] w = new int[N];
for (int i = 0; i < N; i++) {
w[i] = sc.nextInt();
}
System.out.println(solve(w, N));
}
public static int solve(int[] w, int N) {
int[] dp = new int[N];
int res = 0;
for (int i = 0; i < N; i++) {
dp[i] = w[i];
for (int j = 0; j < i; j++) {
if (w[j] < w[i]) {
dp[i] = Math.max(dp[i], dp[j]+w[i]);
}
}
res = Math.max(res, dp[i]);
}
return res;
}
}
1010. 拦截导弹
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。
但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。
某天,雷达捕捉到敌国的导弹来袭。
由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入导弹依次飞来的高度(雷达给出的高度数据是不大于 30000 的正整数,导弹数不超过 1000),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
输入格式:
共一行,输入导弹依次飞来的高度。
输出格式:
第一行包含一个整数,表示最多能拦截的导弹数。
第二行包含一个整数,表示要拦截所有导弹最少要配备的系统数。
输入样例:
389 207 155 300 299 170 158 65
输出样例:
6
2
解法一
Dilworth 定理,双 DP 解法
//300 250 275 252 200 138 245 := 5,2
//最多击落多少就不多说了
//最少需要的系统数量实际上就是最长上升子序列的个数,这里涉及到 Dilworth 定理
public static int[] solve(int[] w, int N){
int[] down = new int[N];
int[] up = new int[N];
int max = 0, count = 1;
for (int i = 0; i < N; i++) {
up[i] = down[i] = 1;
for (int j = 0; j < i; j++) {
if (w[j] >= w[i]) {
down[i] = Math.max(down[j]+1, down[i]);
} else {
up[i] = Math.max(up[j]+1, up[i]);
}
}
max = Math.max(max, down[i]);
count = Math.max(count, up[i]);
}
return new int[]{max, count};
}
最多击落多少就不多说了,最少需要的系统数其实就是最长上升子序列,因为这个子序列是必不可能在同一套系统中被击落,当遇到这个子序列中的元素的时候,就需要增加一套系统,所以最少就是这个子序列的长度
最少需要的系统数量(最少的覆盖整个序列的不上升序列个数)就是最长的上升子序列长度,涉及到 Dilworth 定理(导弹拦截问题与 Dilworth 定理),感兴趣可以去了解下,定理的定义为:对于任意有限偏序集,其最大反链中元素的数目必等于最小链划分中链的数目
本题中,最少需要的导弹系统数量就是最小链划分中链的数量,其最大反链就是最长上升子序列长度,所以直接求最长上升子序列的长度就 ok 了
解法二
双贪心解法。
①求 LIS 的贪心做法,tail[j]
表示长度为j
的下降序列,每个w[i]
作为结尾的时候优先选择大于w[i]
的最小的tail[j]
(也就是从左到右最第一个大于 w[i] 的),然后接在它后面,这样让长度为 j 的下降序列结尾变小,后面可以接上更多的元素,使得序列可以更长,并且这样不会影响之前已有的序列关系,最终 tail 的长度就是最长上升子序列的长度
②求最少的覆盖全序列的下降子序列个数,tail[j]
表示目前为止第j
个系统的最大结尾元素,每个w[i]
被遍历到的时候都是作为某一个系统的结尾,那么此时就会面临选择,这个时候我们就可以贪心的将其放置到大于w[i]
的最小的 tail 后面,这样避免浪费其他较大的结尾(结尾越大,后面可以接的导弹越多),选择消耗一个最小的结尾,可能有点抽象,举个例子就明白了
这里 1 就面临两个选择,要么接在 2 后面,要么接在 4 后面,很明显这里应该接在 2 后面,如果接在 3 后面那么后面的 3 就只能新建一个系统,会增加系统数量
具体的证明可以采用微调法,证明贪心解是小于等于最优解的,这里不再展开
//9 8 7 9 8 7 9 8 7 55 66 99 88 77 88 963 365 4561
//双贪心写法
public static int[] solve2(int[] w, int N){
int[] res = new int[2];
//长度为 i 的最长下降子序列的最小结尾
int[] tail = new int[N];
int len = 0;
//最长下降子序列(不严格)
for (int i = 0; i < w.length; i++) {
//找 tail 中第一个小于 w[i] 的替换掉,这样后面可以接更多的数
//可以二分优化下,这里就不写了
int idx;
for (idx = 0; idx < len; idx++) {
if (tail[idx] < w[i]) {
break;
}
}
tail[idx] = w[i];
len += (idx == len) ? 1 : 0;
}
res[0] = len;
tail = new int[N];
len = 0;
//最少覆盖全序列的下降子序列个数
for (int i = 0; i < w.length; i++) {
//找 tail 中第一个大于等于 w[i] 的替换掉
//可以二分优化下,这里就不写了
int idx;
for (idx = 0; idx < len; idx++) {
if (tail[idx] >= w[i]) {
break;
}
}
tail[idx] = w[i];
len += (idx == len) ? 1 : 0;
}
res[1] = len;
return res;
}
187. 导弹防御系统
为了对抗附近恶意国家的威胁,R 国更新了他们的导弹防御系统。
一套防御系统的导弹拦截高度要么一直 严格单调 上升要么一直 严格单调 下降。
例如,一套系统先后拦截了高度为 3 和高度为 4 的两发导弹,那么接下来该系统就只能拦截高度大于 4 的导弹。
给定即将袭来的一系列导弹的高度,请你求出至少需要多少套防御系统,就可以将它们全部击落。
输入格式
输入包含多组测试用例。
对于每个测试用例,第一行包含整数 n,表示来袭导弹数量。
第二行包含 n 个不同的整数,表示每个导弹的高度。
当输入测试用例 n=0 时,表示输入终止,且该用例无需处理。
输出格式
对于每个测试用例,输出一个占据一行的整数,表示所需的防御系统数量。
数据范围: 1≤n≤50
输入样例:
5
3 5 2 4 1
0
输出样例:
2
样例解释
对于给出样例,最少需要两套防御系统。
一套击落高度为 3,4 的导弹,另一套击落高度为 5,2,1 的导弹。
解法一
搜索 + 贪心剪枝,搜索的复杂度是 O(N*2^N),每个位置有两种选择,每次选择都需要遍历 up 或者 down,虽然时间复杂度很高,但是整体的结果比较小,所以很多情况都被剪掉了,整体速度还不错
import java.util.*;
import java.io.*;
//1. 考虑每个点是在上升序列中还是下降序列中
//2. 考虑是否需要新建一个单独的序列
class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
int N = sc.nextInt();
if (N == 0) return;
int[] w = new int[N];
up = new int[N];
down = new int[N];
res = Integer.MAX_VALUE;
for (int i = 0; i < N; i++) {
w[i] = sc.nextInt();
}
dfs(w, 0, 0, 0);
System.out.println(res);
}
}
static int[] up = null;
static int[] down = null;
static int res;
public static void dfs(int[] w, int c, int ul, int dl) {
//关键的剪枝
if (ul+dl >= res) return;
if (c == w.length) {
res = Math.min(ul+dl, res);
return;
}
//将 c 放在上升序列中
boolean flag = false;
for (int i = 0; i < ul; i++) {
if (up[i] > w[c]) {
flag = true;
int temp = up[i];
up[i] = w[c];
dfs(w, c+1, ul, dl);
up[i] = temp;
break;
}
}
if (!flag) {
up[ul] = w[c];
dfs(w, c+1, ul+1, dl);
up[ul] = 0;
}
flag = false;
for (int i = 0; i < dl; i++) {
if (down[i] < w[c]) {
flag = true;
int temp = down[i];
down[i] = w[c];
dfs(w, c+1, ul, dl);
down[i] = temp;
break;
}
}
if (!flag) {
down[dl] = w[c];
dfs(w, c+1, ul, dl+1);
down[dl] = 0;
}
}
}
解法二
迭代加深搜索,代码简化,在解比较小的时候可以使用这种搜索方式找最小值,结合了 DFS 和 BFS 的优点
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
int N = sc.nextInt();
if (N == 0) return;
int[] w = new int[N];
up = new int[N];
down = new int[N];
res = Integer.MAX_VALUE;
for (int i = 0; i < N; i++) {
w[i] = sc.nextInt();
}
//dfs(w, 0, 0, 0);
int depth = 0;
while (!dfs(w, depth, 0, 0, 0)) {
depth++;
}
System.out.println(depth);
}
}
static int[] up = null;
static int[] down = null;
//迭代加深,BFS 版的 DFS
public static boolean dfs(int[] w, int depth, int c, int ul, int dl) {
if (ul+dl > depth) return false;
if (c == w.length) {
return true;
}
//将 c 放在上升序列中,那么我们应该找到小于 w[i] 的最大的 tail
int i = 0;
for (i = 0; i < ul; i++) {
if (up[i] < w[c]) break;
}
int temp = up[i];
up[i] = w[c];
if (i == ul) {
if (dfs(w, depth, c+1, ul+1, dl)) {
return true;
}
} else if (dfs(w, depth, c+1, ul, dl)) {
return true;
}
up[i] = temp;
//将 c 放在下降序列中,那么我们应该找到大于 w[i] 的最小的 tail
for (i = 0; i < dl; i++) {
if (down[i] > w[c]) break;
}
temp = down[i];
down[i] = w[c];
if (i == dl) {
if (dfs(w, depth, c+1, ul, dl+1)) {
return true;
}
} else {
if (dfs(w, depth, c+1, ul, dl)) {
return true;
}
}
down[i] = temp;
return false;
}
272. 最长公共上升子序列
熊大妈的奶牛在小沐沐的熏陶下开始研究信息题目。
小沐沐先让奶牛研究了最长上升子序列,再让他们研究了最长公共子序列,现在又让他们研究最长公共上升子序列了。
小沐沐说,对于两个数列 A 和 B,如果它们都包含一段位置不一定连续的数,且数值是严格递增的,那么称这一段数是两个数列的公共上升子序列,而所有的公共上升子序列中最长的就是最长公共上升子序列了。
奶牛半懂不懂,小沐沐要你来告诉奶牛什么是最长公共上升子序列。
不过,只要告诉奶牛它的长度就可以了。
数列 A 和 B 的长度均不超过 3000。
输入格式
第一行包含一个整数 N,表示数列 A,B 的长度。
第二行包含 N 个整数,表示数列 A。
第三行包含 N 个整数,表示数列 B。
输出格式
输出一个整数,表示最长公共上升子序列的长度。
数据范围
1≤N≤3000, 序列中的数字均不超过 2^31−1
输入样例:
4
2 2 1 3
2 1 2 3
输出样例:
2
解法一
朴素的解法应该自己想出来的,可惜,没想出来,看了解答才理解,这里dp[i][j]
是指** A 序列的前 i 个字符和 B 的前 j 个字符组成的,以B[j]
结尾的最长公共上升子序列长度**,实际上是 lcs 个 lis 的完美结合,因为状态定义指定了B[j]
是一定要包含的,所以相比 LCS 的四种情况,这里只有两种情况,包含或者不包含A[i]
- 不包含 A[i] 那么状态就是
dp[i-1][j]
; - 如果要包含 A[i],那么
A[i]
必须等于B[j]
,否则就没有考虑的意义,然后我们按照 LIS 的方法去找一个最大值就 ok 了
时间复杂度 N^3,这里按道理是会被卡掉的,但是并没有。..
import java.util.*;
import java.io.*;
class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int N = sc.nextInt();
int[] A = new int[N];
int[] B = new int[N];
for (int i = 0; i < N; i++) {
A[i] = sc.nextInt();
}
for (int i = 0; i < N; i++) {
B[i] = sc.nextInt();
}
System.out.println(solve(A, B, N));
}
public static int solve(int[] A, int[] B, int N){
//A 前 i 个字符和 B 前 j 个字符并且以 B[j] 结尾的最长 LCIS
int[][] dp = new int[N+1][N+1];
int res = 0;
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= N; j++) {
dp[i][j] = dp[i-1][j];
if (A[i-1] == B[j-1]) {
dp[i][j] = Math.max(dp[i][j], 1);
for (int k = 1; k < j; k++) {
if (B[j-1] > B[k-1]) {
dp[i][j] = Math.max(dp[i][k]+1, dp[i][j]);
}
}
}
res = Math.max(res, dp[i][j]);
}
}
return res;
}
}
解法二
对上面的解法做了等价变形,首先上面解法内循环是找[1,j-1]
区间内B[k]<B[j]
的最大值,同时此时的B[j]==A[i]
,所以我们可以进行一波等价变形,动态的计算这个最大值,从而优化掉一层循环,还是很巧妙的
//代码等价变形,时间复杂度 N^2
public static int solveOpt(int[] A, int[] B, int N){
//A 前 i 个字符和 B 前 j 个字符并且以 B[j] 结尾的最长 LCIS
int[][] dp = new int[N+1][N+1];
int res = 0;
for (int i = 1; i <= N; i++) {
int maxv = 1;
for (int j = 1; j <= N; j++) {
dp[i][j] = dp[i-1][j];
if (A[i-1] == B[j-1]) {
dp[i][j] = Math.max(dp[i][j], maxv);
}
if (B[j-1] < A[i-1]) {
maxv = Math.max(maxv, dp[i][j]+1);
}
res = Math.max(res, dp[i][j]);
}
}
return res;
}