【ACM程式設計】動態規劃 第二篇 LCS&LIS問題

tavee發表於2022-05-26

動態規劃

P1439 【模板】最長公共子序列 - 洛谷 | 電腦科學教育新生態 (luogu.com.cn)

題目描述

給出 1,2,…,n 的兩個排列 P1 和 P2 ,求它們的最長公共子序列。

輸入格式

第一行是一個數 n

接下來兩行,每行為 n 個數,為自然數 1,2,…,n 的一個排列。

輸出格式

一個數,即最長公共子序列的長度。

輸入輸出樣例

輸入 #1

5 
3 2 1 4 5
1 2 3 4 5

輸出 #1

3

說明/提示

  • 對於 50% 的資料, n≤1000;
  • 對於 100% 的資料, n≤100000。

首先 我們區分兩個概念:

  • 子序列:序列的一部分項按原有次序排列而得的序列,也是說 這裡 如 3 1 5 也運算元序列
  • 子串:串的連續一部分
  • 排列:從1~n不重複出現

這題如何用dp來解決?首先,我們把序列分為X和Y兩個序列。

我們嘗試尋找一個最優子結構和定義一個狀態來表示公共子序列的大小。

於是,我們可以想到:從兩個串的第一位開始逐個對比到至最後。我們定義狀態d( i , j ),表示兩個串 X 對比到第 i 位, Y 對比到第 j 位這個狀態下的最長公共子序列,那麼d(n,n)即為原題目的解。

我們可以寫出狀態轉移方程:

由此,我們可以寫出程式碼:

#include<stdio.h>
const int N = 1e3 + 7; //1007
int x[N];
int y[N];
int d[N][N];
int max(int x, int y) { return x > y ? x : y; }
int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
        scanf("%d", &x[i]);
    for (int i = 1; i <= n; i++)
        scanf("%d", &y[i]);
    for (int i = 1; i <= n; i++)
        d[i][0] = d[0][i] = 0;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
        {
            if (x[i] == y[j])
                d[i][j] = d[i - 1][j - 1] + 1;
            else d[i][j] = max(d[i - 1][j], d[i][j - 1]);
        }
    printf("%d", d[n][n]);
    return 0;
}

我們嘗試對空間進行優化。

會發現,每次的d(i,j)都是由它左邊d(i,j-1)或者上邊d(i-1,j)或者左上的值轉變而來。

那麼我們是不是就可以用一個二維陣列來滾動代替儲存呢?

這樣,我們就把第一維的空間壓縮為2

由於我們迴圈執行的次數是n^2,所以時間複雜度是O(1e10),必定超時啊。

LCS&LIS問題

如果你能想出樸素的dp演算法那你在第一層,能夠用滾動陣列優化空間那你在第二層,然而出題人在第五層,演算法競賽就是這樣。

我們發現兩個陣列都是全排列陣列,也就是說a中的數字在b中只會出現一次,a中沒出現的數字b中也不會出現,b只是a的另一種排列順序。

那麼我們以a的順序為基準按出現的時機進行記錄後再對b中的數字按照記錄標記那麼b中只有出現時機單調遞增的子序列是符合題目的,這就讓題目從LCS問題變為了LIS問題。

LCS問題:最長公共子序列問題

LIS問題: 最長上升子序列問題

​ ind[num] = i; data[i]=ind[num];

舉個例子 求 1 7 6 2 3 4最長上升子序列

定義狀態:d(i)表示以第i個數字結尾的最長上升子序列

狀態轉移:

初始狀態:

對於每一個數來說,最長上升序列就是本身,即d [ i ] 的初始值為1

#include<stdio.h>
int a[100];
int dp[100];
int max(int x, int y) { return x > y ? x : y; }
int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
        scanf("%d", &a[i]);
    for (int i = 1; i <= n; i++)
    {
        dp[i] = 1;
        for (int j = 1; j < i; j++)
        {
            if (a[j] < a[i])
                dp[i] = max(dp[i], dp[j] + 1);
        }
        printf("dp[%d]=%d", i, dp[i]);
    }
    return 0;
}

num 3 2 1 4 5 1 2 3 4 5

ind[3]=1,ind[2]=2,ind[1]=3,ind[4]=4,ind[5]=5

data[1]=ind[1]=3,data[2]=ind[2]=2,data[3]=ind[3]=1,data[4]=ind[4]=4,data[5]=ind[5]=5

dp[1]=1 i=2 j=1 data[1]>data[2],dp[2]=1 i=3 j=1 dp[3]=1 j=2 dp[3]=1

i=4 j=1 dp[4]=max(dp[4],dp[1]+1)=2 j=2 dp[4]=2

i=5 j=1 dp[5]=3

ans=3

但是 時間還是n^2

1 7 6 2 3 4

為了優化,我們可以另外開一個單調的陣列,用於儲存上升的數。

設定一個答案序列B,初始為空。第一次搜尋到了1,將1加入答案序列,然後到了7,7>1故加入序列,隨後到

了數字6,我們找到序列中第一個大於該數字的數,用該數字進行替換。

這是因為我們只需要求出長度,這樣子替換不會對最終的答案造成影響可以視為答案序列被替換後的6即表示原序列6的位置,也表示7的位置。

最終答案序列是{1,2(6,7),3,4}

假如是5 2 3 1 4,最終答案序列是{1(2,5),3,4} 可以看出1的位置能夠表示2或者5,最終序列的答案也是2,3,4。

由於該佇列的嚴格單調,所以我們使用二分的方法去查詢。

最後的答案即佇列的長度。

1,7,6,2,3,4

i=1 {1}

i=2 {1,7}

i=3 {1,6(7)} 因為6比7小,覆蓋了7可以使來了更大的數可以延長這個序列

i=4 {1,2(6,7)} 同理,其作用在下一行表現出來了

i=5 {1,2(6,7),3}

i=6 {1,2(6,7),3,4}

答案為4。

#include<stdio.h>
const int N = 1e5 + 7;
int data[N], dp[N], ind[N];
int goal[N]; //佇列
int num, ans;
int max(int x, int y) { return x > y ? x : y; }
//二分查詢 
int search(int l, int r, int num)
{
    while (l < r)
    {
        int mid = (l + r) >> 1;
        if (num <= goal[mid]) r = mid;
        else l = mid + 1;
    }
    return l;
}
int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
    {
        scanf("%d", &num);
        ind[num] = i;
    }
    for (int i = 1; i <= n; i++)
    {
        scanf("%d", &num);
        data[i] = ind[num];
    }
    int len = 1;
    goal[1] = data[1];
    int pos = 0;
    for (int i = 2; i <= n; i++)
    {
        //data[i]>隊尾元素故加入序列
        if (goal[len] < data[i])
            goal[++len] = data[i];
        //查詢到第一個大於data[i]的數,用該數字進行替換
        else 
            goal[search(1, len, data[i])] = data[i];
    }
    printf("%d", len);
    return 0;
}

這樣我們的時間可以化為O(n*log n)

相關文章