動態規劃篇——線性DP
本次我們介紹動態規劃篇的線性DP,我們會從下面幾個角度來介紹:
- 數字三角形
- 最長上升子序列I
- 最長上升子序列II
- 最長公共子序列
- 最短編輯距離
數字三角形
我們首先介紹一下題目:
/*題目概述*/
給定一個如下圖所示的數字三角形,從頂部出發,在每一結點可以選擇移動至其左下方的結點或移動至其右下方的結點,一直走到底層
要求找出一條路徑,使路徑上的數字的和最大。
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
/*具體需求*/
// 輸入格式
第一行包含整數 n,表示數字三角形的層數。
接下來 n 行,每行包含若干整數,其中第 i 行表示數字三角形第 i 層包含的整數。
// 輸出格式
輸出一個整數,表示最大的路徑數字和。
// 資料範圍
1 ≤ n ≤ 500
−10000 ≤ 三角形中的整數 ≤ 10000
// 輸入樣例:
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
// 輸出樣例:
30
然後我們進行分析:
/*題目分析*/
我們採用DP思想
首先我們採用a[i][j]來表示第i行,第j列的數字;我們採用f[i][j]表示到達第i行第j列的路徑最大值
那麼我們的f[i][j]就有兩條來源,分別來自於f[i][j]的左上和右上,也就是f[i-1][j-1]和f[i-1][j]
那麼我們的當前值f[i][j]的最大值也就是左上和右上的最大值加上當前a[i][j]即可,注意每一行都是最大值,所以前面f[i][j]也是最大值
注意:由於上面操作涉及到j-1和j,可能會涉及邊界問題,為了減少if判斷條件,我們的操作從下標為1開始!
我們給出具體程式碼:
import java.util.Scanner;
public class NumberTriangle {
final static int N =100010;
final static int INF = Integer.MIN_VALUE/2;
// 提前設定資訊,a為當前值,f為路徑max
static int n;
static int[][] a = new int[N][N];
static int[][] f = new int[N][N];
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
// a賦值
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i; j++) {
a[i][j] = scanner.nextInt();
}
}
// f初始值(注意,這裡的初始值需要聯通邊界也設定好初始值)
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= i+1; j++) {
f[i][j] = INF;
}
}
// 開始DP
f[1][1] = a[1][1];
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= i; j++) {
f[i][j] = Math.max(f[i-1][j-1],f[i-1][j]) + a[i][j];
}
}
// 提供返回值即可
int res = INF;
for (int i = 1; i <= n; i++) {
res = Math.max(res,f[n][i]);
}
System.out.println(res);
}
}
最長上升子序列I
我們首先介紹一下題目:
/*題目概述*/
給定一個長度為 N 的數列,求數值嚴格單調遞增的子序列的長度最長是多少。
/*具體需求*/
// 輸入格式
第一行包含整數 N。
第二行包含 N 個整數,表示完整序列。
// 輸出格式
輸出一個整數,表示最大長度。
// 資料範圍
1 ≤ N ≤ 1000,
−109 ≤ 數列中的數 ≤ 109
// 輸入樣例:
7
3 1 2 1 8 5 6
// 輸出樣例:
4
然後我們進行分析:
/*題目分析*/
我們採用DP思想
我們採用a[i]表示第i個數的值,我們採用f[i]表示以當前值結尾的最長子序列長度
那麼我們就需要採用雙重迴圈,第一層迴圈用來遍歷i,更新f[i];第二層迴圈用來查詢i之前的j,判斷j<i,則進行f[i]更新
我們給出具體程式碼:
import java.util.Scanner;
public class Main {
final static int N = 1010;
static int n;
static int[] a = new int[N];
static int[] f = new int[N];
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
// 賦值
for (int i = 1; i <= n; i++) {
a[i] = scanner.nextInt();
}
// 開始DP
for (int i = 1; i <= n; i++) {
// 最開始只有他自己,預設為1
f[i] = 1;
// 二重迴圈,更新fi
for (int j = 1; j < i; j++) {
if (a[j] < a[i]) f[i] = Math.max(f[i],f[j] + 1);
}
}
// 最後輸出結果即可
int res = 0;
for (int i = 1; i <= n; i++) {
res = Math.max(res,f[i]);
}
System.out.println(res);
}
}
最長上升子序列II
我們這裡對最長上升子序列進行一個最佳化處理:
/*最佳化思路*/
我們在之前是與所有小於該點的數進行一一比較,也就是雙迴圈
我們可以採用q陣列來存放不同子序列長度下的最小值來作為判定條件,同時我們採用二分查詢來最佳化查詢時間複雜度
/*程式碼展示*/
import java.util.Scanner;
public class Main {
final static int N = 100010;
// a存放當前陣列,q存放每種長度的最長上升子序列中結尾的最小值
static int n;
static int[] a = new int[N];
static int[] q = new int[N];
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
for (int i = 0; i < n; i++) {
a[i] = scanner.nextInt();
}
// 我們首先需要設定q的前置條件,我們將長度設定0,將q[0]設定為負無窮以便於a的值可以存放進q中
int len = 0;
q[0] = -(int)-2e9;
// 一個數可以接在什麼位置,用二分來尋找每種長度的結尾的最小值比這個數小的的位置,然後長度加1,
// 就是新的最長上升子序列的長度
for(int i = 0 ; i < n ; i ++ ){
int l = 0,r = len;
while(l < r){
int mid = l + r + 1 >> 1;
if(q[mid] < a[i]) l = mid;
else r = mid - 1;
}
// 找到位置之後更新長度
len = Math.max(len, r + 1);
// 比如因為找到的數q[4]是小於的a最大的數,所以後面的一個數q[5]就是大於等於這個數
// 然後我們可以接在q[4]後面,所以現在長度是5,變成q[5],然後因為我們的這個數是小於或者等於q[5]
// 所以直接將值賦上就行
q[r + 1] = a[i];
}
System.out.println(len);
}
}
最長公共子序列
我們首先介紹一下題目:
/*題目概述*/
給定兩個長度分別為 N 和 M 的字串 A 和 B,求既是 A 的子序列又是 B 的子序列的字串長度最長是多少。
/*具體需求*/
// 輸入格式
第一行包含兩個整數 N 和 M。
第二行包含一個長度為 N 的字串,表示字串 A。
第三行包含一個長度為 M 的字串,表示字串 B。
字串均由小寫字母構成。
// 輸出格式
輸出一個整數,表示最大長度。
// 資料範圍
1 ≤ N, M ≤ 1000
// 輸入樣例:
4 5
acbd
abedc
// 輸出樣例:
3
然後我們進行分析:
/*題目分析*/
我們採用DP思想
我們使用f[i][j]來表示a字串前i個字元和b字串前j個字元之間的最大子序列長度
那麼我們希望用之前的f來更新最新的f,我們主要分為四種狀態;
f[i][j] = f[i-1][j-1]
f[i][j] = f[i-1][j]
f[i][j] = f[i][j-1]
f[i][j] = f[i-1][j-1]+1
我們需要注意的是:
f[i-1][j]和f[i][j-1]已經涵括了f[i-1][j-1],所以我們可以少寫一種情況
f[i][j] = f[i-1][j-1]+1情況只有當a[i]==b[i]時才會觸發
我們給出具體程式碼:
import java.util.Scanner;
public class Main {
final static int N = 1010;
static int n,m;
static char[] a = new char[N];
static char[] b = new char[N];
static int[][] f = new int[N][N];
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 賦值
n = scanner.nextInt();
m = scanner.nextInt();
String A = scanner.next();
for (int i = 1; i <= n; i++) {
a[i] = A.charAt(i-1);
}
String B = scanner.next();
for (int i = 1; i <= m; i++) {
b[i] = B.charAt(i-1);
}
// DP演算法
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
// 第一種情況:f[i-1][j]和f[i][j-1]
f[i][j] = Math.max(f[i-1][j],f[i][j-1]);
// 第二種情況:f[i-1][j-1] + 1
if (a[i] == b[j]) f[i][j] = Math.max(f[i-1][j-1]+1,f[i][j]);
}
}
// 輸出
System.out.println(f[n][m]);
}
}
最短編輯距離
我們首先介紹一下題目:
/*題目概述*/
給定兩個字串 A 和 B,現在要將 A 經過若干操作變為 B,可進行的操作有:
刪除–將字串 A 中的某個字元刪除。
插入–在字串 A 的某個位置插入某個字元。
替換–將字串 A 中的某個字元替換為另一個字元。
現在請你求出,將 A 變為 B 至少需要進行多少次操作。
/*具體需求*/
// 輸入格式
第一行包含整數 n,表示字串 A 的長度。
第二行包含一個長度為 n 的字串 A。
第三行包含整數 m,表示字串 B 的長度。
第四行包含一個長度為 m 的字串 B。
字串中均只包含大小寫字母。
// 輸出格式
輸出一個整數,表示最少操作次數。
// 資料範圍
1 ≤ n,m ≤ 1000
// 輸入樣例:
10
AGTCTGACGC
11
AGTAAGTAGGC
// 輸出樣例:
4
然後我們進行分析:
/*題目分析*/
我們採用DP思想
這裡的DP思想其實和最長公共子序列很相似
我們使用f[i][j]來表示a字串前i個字元和b字串前j個字元之間進行匹配的最小運算元
我們需要提前設定一下初始值:
f[i][0]表示a的前i個字元和b為空時,這時我們需要對a進行i次減法:f[i][0] = i;
f[0][j]表示a為空和b的前j個字元時,這時我們需要對a進行i次加法:f[0][j] = i;
那麼我們希望用之前的f來更新最新的f,我們主要分為兩種狀態;
當i和j變更時,我們需要對a做新增或刪除:f[i][j] = Math.min(f[i - 1][j] + 1, f[i][j - 1] + 1);
當i和j同時變更,且a[i]==b[j],這時不需要操作:(a[i] == b[j]) f[i][j] = Math.min(f[i][j],f[i - 1][j - 1]);
但是如果不相等,我們需要進行修改操作:else f[i][j] = Math.min(f[i][j],f[i - 1][j - 1] + 1);
我們給出具體程式碼:
import java.util.*;
public class UpdateShort{
public static void main(String[] args){
Scanner scanner = new Scanner(System.in);
int N = 1010;
char[] a = new char[N];
char[] b = new char[N];
int[][] f = new int[N][N];
int n = scannernextInt();
String A = scanner.next();
int m = scanner.nextInt();
String B = scanner.next();
for(int i = 1 ; i <= n ; i ++ ) {
a[i] = A.charAt(i - 1);
f[i][0] = i; // 處理邊界,字串b是0,a進行n次刪除
}
for(int i = 1 ; i <= m ; i ++ ){
b[i] = B.charAt(i - 1);
f[0][i] = i; // 處理邊界,字串a是0,a進行m次增加
}
for(int i = 1 ; i <= n ; i ++ ){
for(int j = 1 ; j <= m ; j ++ ){
// 刪除和增加操作
f[i][j] = Math.min(f[i - 1][j] + 1, f[i][j - 1] + 1);
// 最後一個數相同,不用進行修改操作,則不用加1
if(a[i] == b[j]) f[i][j] = Math.min(f[i][j],f[i - 1][j - 1]);
else f[i][j] = Math.min(f[i][j],f[i - 1][j - 1] + 1); // 修改操作
}
}
System.out.println(f[n][m]);
}
}
結束語
好的,關於動態規劃篇的線性DP就介紹到這裡,希望能為你帶來幫助~