動態規劃_線性dp

http://www.javashuo.com/article/p-welkkfze-hd.htmlphp

線性dp是很基礎的一種動態規劃,,經典題和他的變種有不少,好比兩個串的LCS,LIS,最大子序列和等等,,html

線性dp是用來解決一些 線性區間上的最優化問題 ,,node

學這裏的東西我感受主要要理解好問題的子問題來寫出轉移方程,,還有弄清具體的邊界條件就好了,,ios

LCS-最長公共子序列

分析

子序列指的是對於一個串,某些元素的排列與原串所在的順序一致的串稱爲原串的一個子序列,,它與子串不一樣,子串必須保證個元素在原串中是連續的,,,eg: 原串:abcdef 一個子序列:acf 一個子串:abcdc++

兩個串的最大公共子序列指的是對於兩個串全部相同的子序列中最長的那一個,,數組

參考1
參考2優化

首先肯定子問題

既然要用動態規劃解決,那麼這個問題必定可以分紅子問題來推出。。首先根據定義能夠看出對於兩個串的子串的LCS也必定是原串的LCS的一部分,,這樣咱們就能夠用原串的子串的LCS來求原串的LCS了,,spa

狀態

咱們用 \(dp[i][j]\) 來表示對於A的子串 \(A':A_1, A_2, A_3,,,A_i\) 和B的子串 \(B':B_1, B_2, B_3,,,B_j\)LCS.net

那麼怎麼經過上一狀態獲得 \(dp[i][j]\) 呢?往前推一個字符看看code

考慮全部 \(A',B'\) 的子串,他們的可能狀況有;

  • 兩個串的某尾字符同樣 \((a[i]=b[j])\),,顯然這樣狀況下 \(dp[i][j]=dp[i-1][j-1]+1\)
  • 不相等時就找 \(A'\) 往前推一個字符和 \(B'\)的LCS 與 \(A'\)\(B'\) 往前推一個字符的LCS 的最大的那個就好了,,也就是說 \(dp[i][j]=max(dp[i-1][j], dp[i][j-1])\)

狀態轉移方程

狀態轉移方程爲:

\[ { dp[i][j]= \begin{cases} dp[i-1][j-1]+1, & \text{if a[i]=b[j]}\\ max(dp[i-1][j], dp[i][j-1], & \text{if a[i] != b[j]})\\ \end{cases} } \]

注意初始化的時候dp[i][j]=0;

例題

hdu-1159

板子題直接作就行,,熟悉一下代碼

const int maxn = 1e4 + 5;
const int maxm = 2e5 + 5;
const ll mod = 1e9 + 7;
int dp[maxn][maxn];
char a[maxn], b[maxn];
int main()
{
//    freopen("233.in" , "r" , stdin);
//    freopen("233.out" , "w" , stdout);
//    ios_base::sync_with_stdio(0);
//    cin.tie(0);cout.tie(0);
    while(~scanf("%s%s", a, b))
    {
        int len1 = strlen(a);
        int len2 = strlen(b);
        for(int i = 0; i <= max(len1, len2); ++i)
            for(int j = 0; j <= max(len1, len2); ++j)
                dp[i][j] = 0;
        for(int i = 1; i <= len1; ++i)
            for(int j = 1; j <= len2; ++j)
                if(a[i - 1] == b[j - 1])
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                else
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
        printf("%d\n", dp[len1][len2]);
    }
    return 0;
}

poj-2250

題意:兩個沒有標點只有空格的並以'#"結尾的句子,讓你找出LCS,並輸出

解決的方法就是LCS,基本的套路沒變,,就是對數據的處理改一下,,用一個字符串數組存一下,,

而後最後要將序列輸出時,用一個mark數組標記每一次dp時的狀況(記錄下每一個狀態的最優值是由狀態轉移方程的哪一項推出的),,最後逆着返回去把答案記錄一下就好,,(把mark數組手推一下就行,,(揹包九講裏最後提到過解的輸出,,,

這個很重要,,不少地方都會用到,,,

const int maxn = 1e4 + 5;
const int maxm = 2e5 + 5;
const ll mod = 1e9 + 7;
int dp[maxn][maxn];
string a[maxn], b[maxn];
int mark[maxn][maxn];
int cnt, ans[maxn];
void findans(int i, int j)
{
    if(!i && !j)return;
    if(mark[i][j] == 0)
    {
        findans(i - 1, j - 1);
        ans[++cnt] = i;
    }
    else if(mark[i][j] == 1)
        findans(i - 1, j);
    else
        findans(i, j - 1);
}
int main()
{
//    freopen("233.in" , "r" , stdin);
//    freopen("233.out" , "w" , stdout);
//    ios_base::sync_with_stdio(0);
//    cin.tie(0);cout.tie(0);
    while(cin >> a[1])
    {
        int len1 = 1;
        int len2 = 1;
        while(a[len1] != "#")cin >> a[++len1];--len1;
        cin >> b[1];
        while(b[len2] != "#")cin >> b[++len2];--len2;
        for(int i = 0; i <= max(len1, len2); ++i)
            for(int j = 0; j <= max(len1, len2); ++j)
                dp[i][j] = 0;
        for(int i = 1; i <= len1; ++i)mark[i][0] = 1;
        for(int i = 1; i <= len2; ++i)mark[0][i] = -1;
        for(int i = 1; i <= len1; ++i)
            for(int j = 1; j <= len2; ++j)
                if(a[i] == b[j])
                {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                    mark[i][j] = 0;
                }
                else if(dp[i - 1][j] >= dp[i][j - 1])
                {
                    dp[i][j] = dp[i - 1][j];
                    mark[i][j] = 1;
                }
                else
                {
                    dp[i][j] = dp[i][j - 1];
                    mark[i][j] = -1;
                }
        cnt = 0;
        findans(len1, len2);
        cout << a[ans[1]];
        for(int i = 2; i <= cnt; ++i)cout << " " << a[ans[i]];
        cout << endl;
    }
    return 0;
}

hdu-1503

題意就是給定兩個串,,輸出一個串,這個串的其中兩個子序列要是原來的兩個串,,

要輸出答案,,因此要在狀態轉移的時候標記每一個字符,,最後回溯時判斷輸出就好了,,,

//hdu
//#include <bits/stdc++.h>
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <string.h>
#include <algorithm>
#include <queue>
#define aaa cout<<233<<endl;
#define endl '\n'
#define pb push_back
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 0x3f3f3f3f;//1061109567
const ll linf = 0x3f3f3f3f3f3f3f;
const double eps = 1e-6;
const double pi = 3.14159265358979;
const int maxn = 1e4 + 5;
const int maxm = 2e5 + 5;
const ll mod = 1e9 + 7;
int dp[maxn][maxn];
char a[maxn], b[maxn];
int mark[maxn][maxn];
int cnt, ans[maxn];
void findans(int i, int j)
{
    if(!i && !j)return;
    if(mark[i][j] == 0)
    {
        findans(i - 1, j - 1);
        printf("%c", a[i - 1]);
    }
    else if(mark[i][j] == 1)
    {
        findans(i - 1, j);
        printf("%c", a[i - 1]);
    }
    else
    {
        findans(i, j - 1);
        printf("%c", b[j - 1]);
    }
}
int main()
{
//    freopen("233.in" , "r" , stdin);
//    freopen("233.out" , "w" , stdout);
//    ios_base::sync_with_stdio(0);
//    cin.tie(0);cout.tie(0);
    while(~scanf("%s%s", a, b))
    {
        int len1 = strlen(a);
        int len2 = strlen(b);
        for(int i = 0; i <= max(len1, len2); ++i)
            for(int j = 0; j <= max(len1, len2); ++j)
                dp[i][j] = 0;
        for(int i = 1; i <= len1; ++i)mark[i][0] = 1;
        for(int i = 1; i <= len2; ++i)mark[0][i] = -1;
        for(int i = 1; i <= len1; ++i)
            for(int j = 1; j <= len2; ++j)
                if(a[i - 1] == b[j - 1])
                {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                    mark[i][j] = 0;
                }
                else if(dp[i - 1][j] >= dp[i][j - 1])
                {
                    dp[i][j] = dp[i - 1][j];
                    mark[i][j] = 1;
                }
                else
                {
                    dp[i][j] = dp[i][j - 1];
                    mark[i][j] = -1;
                }
        findans(len1, len2);
        printf("\n");
    }
    return 0;
}

hdu-1513

題意:給你一個長度爲n的字符串,問你最少添加幾個字符使得這個字符串變成一個迴文串,,

由於只是問字符的個數,,沒問最後的結果,,因此能夠先求原串和其逆串的LCS,,而後用長度建議下就好了,,,

注意,由於字符串的長度是小於等於5000,,開dp數組時直接開會爆掉,,因此要用 滾動數組 來優化一下空間,,
(看一下那個dp的圖就能看出在求dp[i][j]是,,僅僅用到的是上一行,,在往上就再也不用了,,因此能夠直接用兩行解決就好了,,,好比說奇數行用第一層,偶數用第零層,,i%2就行,,訪問當前層的上一層就用 1-i%2 就好了,,很巧啊,,

const int maxn = 1e4 + 5;
const int maxm = 2e5 + 5;
const ll mod = 1e9 + 7;
int dp[2][maxn];
char a[maxn], b[maxn];
int main()
{
//    freopen("233.in" , "r" , stdin);
//    freopen("233.out" , "w" , stdout);
//    ios_base::sync_with_stdio(0);
//    cin.tie(0);cout.tie(0);
    int n;
    while(~scanf("%d", &n))
    {
        scanf("%s", a);
        for(int i = 0; i <= n - 1; ++i)b[i] = a[n - i - 1];
        int len1 = n;
        int len2 = len1;
        for(int i = 0; i <= max(len1, len2); ++i)
            dp[0][i] = dp[1][i] = 0;
        for(int i = 1; i <= len1; ++i)
            for(int j = 1; j <= len2; ++j)
                if(a[i - 1] == b[j - 1])
                {
                    dp[i % 2][j] = dp[1 - i % 2][j - 1] + 1;
                }
                else
                    dp[i % 2][j] = max(dp[1 - i % 2][j], dp[i % 2][j - 1]);
        printf("%d\n", n - dp[n % 2][n]);
    }
    return 0;
}

最長公共子串

子序列是序列中的元素不必定連續,,子串的話每個元素在原串中是連續的,,能夠修改一下LCS來求

狀態轉移方程

由於要保證連續,因此只有在 \(a[i]=b[j]\) 時,\(dp[i][j] = dp[i-1][j-1]\),,也就是說 \(dp[i][j]\) 表示長度爲i和j的子串的最長子串

代碼

for(int i = 1; i <= len1; ++i)
    for(int j = 1; j <= len2; ++j)
    {
        if(a[i-1] = b[j-1])
            dp[i][j] = dp[i-1][j-1]+1;
        else
            dp[i][j] = 0;
        ans = max(ans, dp[i][j]);
    }

LIS-最長上升序列

分析

上升序列就是指序列的元素時遞增的,,例如:4,1,3,2,5,7中的一個上升序列就是1,2,5,7,,

肯定子問題

某個從1開始的子串的LIS必定是原串LIS的子序列,,因此能夠經過枚舉右邊界來獲得原串的LIS,,

狀態

\(dp[i]\) 表示 \(A_1, A_2, A_3,,,A_i\)這個子串的LIS,,而後枚舉這個子串中的元素,,若是 \(a[j]<a[i]\) ,即第i個元素比第j個元素大的時候,能夠將第i個元素做爲某個子序列的一部分,,

狀態轉移方程

\[ { dp[i]= \begin{cases} max(dp[i], dp[j]+1) & \text{if a[i] > a[j]}\\ \end{cases} } \]

由於最後最長的序列並不必定是以a[n]結尾的,,因此最後的最大值並不必定是dp[n],,要遍歷一遍整個dp數組找一下,,,

時間複雜度

這樣作的時間複雜度大概是 \(O(n^2)\),,,能夠再用二分或則樹狀數組維護下降時間複雜度

例題

poj-2533

裸dp作法,時間複雜度 \(O(n^2)\)

裸板子題,,注意初始化dp數組的數後是初始化爲1,,不是像LCS初始化爲0;

int n;
while(~scanf("%d", &n))
{
    for(int i = 1; i <= n; ++i)scanf("%d", &a[i]);
    for(int i = 0; i <= n; ++i)dp[i] = 1;
    for(int i = 2; i <= n; ++i)
        for(int j = 1; j < i; ++j)
            if(a[i] > a[j])dp[i] = max(dp[i], dp[j] + 1);
    int ans = 0;
    for(int i = 1; i <= n; ++i)ans = max(ans, dp[i]);
    printf("%d\n", ans);
}

貪心+二分,時間複雜度 \(O(nlogn)\)

裸的dp的內層循環的做用是尋找在 \(a[i]>a[j]\) 時的最大的 \(dp[j]\) 的值,,單純的遍歷複雜度會增一倍,,

能夠用一個數組保存i以前最長的上升子序列,,,

若是此時的 \(a[i]\) 比那個數組的最大的元素也就是最後一個元素的值大的話,,就直接加在那個數組後面,,

不然,就想方法替換掉裏面接近 \(a[i]\) 的元素,,,能夠用二分來優化這一過程,,

具體的能夠參考這裏
和這裏

int n;
while(~scanf("%d", &n))
{
    cnt = 1;
    for(int i = 1; i <= n; ++i)scanf("%d", &a[i]);
    b[1] = a[1];
    for(int i = 1; i <= n; ++i)
    {
        if(a[i] > b[cnt])b[++cnt] = a[i];
        else
        {
            int k = lower_bound(b + 1, b + 1 + cnt, a[i]) - b;
            b[k] = a[i];
        }
    }
    printf("%d\n", cnt);
}

樹狀數組維護,時間複雜度 \(O(nlogn)\)

~~(loading),,,

看到有這個作法,,可是不知道怎麼是錯的,,,(好像是排序後要去重???否則是求得最長不降低子序列~~

算了,先貼個 的代碼吧,,,,

const int maxn = 1e4 + 5;
const int maxm = 2e5 + 5;
const ll mod = 1e9 + 7;

struct node
{
    int id, num;
    const bool operator<(const node &r)const
    {
        return num < r.num;
    }
    const bool operator==(const node &r)const
    {
        return num == r.num;
    }
}node[maxn];
int bit[maxn];
int n;
void update(int i, int x)
{
    for(; i <= n; i += i & (-i))bit[i] = max(bit[i], x);
}
int query(int i)
{
    int res = -inf;
    for(; i; i -= i & (-i))res = max(res, bit[i]);
    return res;
}
int main()
{
//    freopen("233.in" , "r" , stdin);
//    freopen("233.out" , "w" , stdout);
//    ios_base::sync_with_stdio(0);
//    cin.tie(0);cout.tie(0);
    while(~scanf("%d", &n))
    {
        for(int i = 1; i <= n; ++i)
        {
            scanf("%d", &node[i].num);
            node[i].id = i;
        }
        //memset(bit, 0, sizeof bit);
        for(int i = 1; i <=100; ++i)bit[i] = 1;
        sort(node + 1, node + 1 + n);
//        int cnt = unique(node + 1, node + 1 + n) - node - 1;
        int ans = 0;
        for(int i = 1; i <= n; ++i)
        {
            cout << node[i].num;
            if(node[i].num > node[i - 1].num)
            {
                int mx = query(node[i].id);
                update(node[i].id, ++mx);
                ans = max(ans, mx);
            }


        }
        printf("%d\n", ans);
    }
    return 0;
}
4
1 1 1 1
//出來的結果是4,,,

LICS-最長公共上升子序列

LICS就是將LIS和LCS合在一塊兒,,稍微改一改就好了,,

分析

子問題

像LCS,LIS同樣,,咱們用dp[i][j]表示序列1取長度爲i和序列2取長度爲j時的LICS的值,,而後枚舉每個元素來更新後面的獲得最後的答案,,

狀態轉移方程

  • \(a[i]=b[j]\)時,,顯然此時的LICS就爲前面出現的最大的LICS的值加一,,也就是: \(dp[i][j]=max(d[i][k])+1 \{ k = 1 \ to \ j - 1 \}\)

若是隻是單純的一遍一遍的枚舉k,,顯然會使最後的時間複雜度增長爲 \(O(n^3)\) ,, 由於每次更新dp[i][j]都是尋找的前面的最值,,因此咱們能夠記錄下來前面的最值,,而後和當點枚舉的比較就好了,,,

爲了保證時上升的,,因此不等的時候只能尋找 \(a[i]>b[j]\) 的狀況,,找到最大值

例題

hdu-1423

板子題,,直接作

//沒有空間優化的
//注意輸出格式
int a[maxn], b[maxn], dp[maxn][maxn];
int main()
{
    int t;scanf("%d", &t);
    while(t--)
    {
        int len1, len2;
        scanf("%d", &len1);
        for(int i = 1; i <= len1; ++i)scanf("%d", &a[i]);
        scanf("%d", &len2);
        for(int i = 1; i <= len2; ++i)scanf("%d", &b[i]);
        for(int i = 0; i <= len1; ++i)
            for(int j = 0; j <= len2; ++j)
                dp[i][j] = 0;
        for(int i = 1; i <= len1; ++i)
        {
            int mx = 0;
            for(int j = 1; j <= len2; ++j)
            {
                dp[i][j] = dp[i - 1][j];//先保存前面的最值,而後判斷更新
                if(a[i] == b[j])dp[i][j] = mx + 1;
                if(a[i] >  b[j])mx = max(mx, dp[i - 1][j]);
            }
        }
        int ans = 0;
        for(int i = 1; i <= len2; ++i)
            ans = max(ans, dp[len1][i]);
        printf("%d\n", ans);
        if(t)printf("\n");

    }
    return 0;
}

注意到在循環中的一句: dp[i][j]=dp[i-1][j],,這句能夠看出咱們的dp過程是沒有用到前面幾層的,,,也就是說能夠用一個覺得數組來優化一下,,,有點相似01揹包的空間優化過程

int a[maxn], b[maxn], dp[maxn];
int main()
{
//    freopen("233.in" , "r" , stdin);
//    freopen("233.out" , "w" , stdout);
//    ios_base::sync_with_stdio(0);
//    cin.tie(0);cout.tie(0);
    int t;scanf("%d", &t);
    while(t--)
    {
        int len1, len2;
        scanf("%d", &len1);
        for(int i = 1; i <= len1; ++i)scanf("%d", &a[i]);
        scanf("%d", &len2);
        for(int i = 1; i <= len2; ++i)scanf("%d", &b[i]);
        for(int i = 0; i <= len2; ++i)
            dp[i] = 0;
        for(int i = 1; i <= len1; ++i)
        {
            int mx = 0;
            for(int j = 1; j <= len2; ++j)
            {
                if(a[i] == b[j])dp[j] = mx + 1;
                if(a[i] >  b[j])mx = max(mx, dp[j]);
            }
        }
        int ans = 0;
        for(int i = 1; i <= len2; ++i)
            ans = max(ans, dp[i]);
        printf("%d\n", ans);
        if(t)printf("\n");

    }
    return 0;
}

最大連續子序列和

最大連續子序列和求得是一段連續的子序列,,它的和是全部子序列中最大的,,例如:-2 11 -4 13 -5 -2中,最大的連續子序列和是20,,由11,-4,13組成,,

參考文章

例題hdu-1231

法一

咱們能夠遍歷整個序列,,而且保存從頭到當前點的序列中的 最大連續子序列和sum,同時保存起點終點元素值,,

當sum<=0時,,說明前面一個子序列的和小於零,就能夠再也不要他了,,此時更新新的sum爲當前點,起點終點也爲當前點的值,,

當sum>0時,,咱們能夠再把當前點加在這個序列後面,,更新終點便可,,

最後取每一次枚舉中的最大值,,更新起點終點就好了,,,

若是最值小於零,按題意輸出零便可,,

const int maxn = 1e5 + 5;
const int maxm = 2e5 + 5;
const ll mod = 1e9 + 7;
int a[maxn], b[maxn], dp[maxn];
int main()
{
//    freopen("233.in" , "r" , stdin);
//    freopen("233.out" , "w" , stdout);
//    ios_base::sync_with_stdio(0);
//    cin.tie(0);cout.tie(0);
    int n;
    while(~scanf("%d", &n) && n)
    {
        for(int i = 1; i <= n; ++i)scanf("%d", &a[i]);
        int sum, max_sum, s, t, ans_s, ans_t;
        sum = max_sum = s = t = ans_s = ans_t = a[1];
        for(int i = 2; i <= n; ++i)
        {
            if(sum > 0)
            {
                sum += a[i];
                t = a[i];
            }
            else
            {
                sum = s = t = a[i];
            }
            //update ans
            if(max_sum < sum)
            {
                max_sum = sum;
                ans_s = s;
                ans_t = t;
            }
        }
        if(max_sum < 0)printf("0 %d %d\n", a[1], a[n]);
        else           printf("%d %d %d\n", max_sum, ans_s, ans_t);
    }
    return 0;
}

法二

可使用dp來解決,,就像LCS,LIS等dp[i]表明以第i個元素結尾的LCS,LIS同樣,,這裏能夠用dp[i]表示以a[i]結尾的最大的連續序列的和,,這樣爲了推出dp[i]就得看它和dp[i-1]的關係,,

從上面那種解法能夠看出,當dp[i-1]小於零時意味着以a[i]結尾的最大連續序列的和就是負的,,爲了答案的最大化,,能夠捨棄前面這一段,,因此在這種狀況下的dp[i]=a[i],,,

不然的話,就把當前點a[i]加到前面的序列上,也就是dp[i]=dp[i-1]+a[i],,,

因而最後的狀態轉移方程爲:

\[ dp[i]= \begin{cases} a[i] & \text{if dp[i-1]<0}\\ dp[i-1]+a[i] & \text{if dp[i-1]>=0}\\ \end{cases} \]

最後針對這道題遍歷一遍dp數組,找到最大值及其下標,,反向遍歷找到起點就行了

const int maxn = 1e5 + 5;
const int maxm = 2e5 + 5;
const ll mod = 1e9 + 7;
int a[maxn], b[maxn], dp[maxn];
int main()
{
//    freopen("233.in" , "r" , stdin);
//    freopen("233.out" , "w" , stdout);
//    ios_base::sync_with_stdio(0);
//    cin.tie(0);cout.tie(0);
    int n;
    while(~scanf("%d", &n) && n)
    {
        for(int i = 1; i <= n; ++i)scanf("%d", &a[i]);
        for(int i = 0; i <= n; ++i)dp[i] = -inf;
        for(int i = 1; i <= n; ++i)
            if(dp[i - 1] < 0)dp[i] = a[i];
            else             dp[i] = dp[i - 1] + a[i];
        int max_sum = -inf, s, t;
        for(int i = 1; i <= n; ++i)
            if(max_sum < dp[i])
                max_sum = dp[i], t = i;
        if(max_sum < 0)printf("0 %d %d\n", a[1], a[n]);
        else
        {
            printf("%d ", max_sum);
            max_sum -= a[t];
            for(int i = t; i >= 1; --i, max_sum -= a[i])
                if(!max_sum)
                {
                    s = i;
                    break;
                }
            printf("%d %d\n", a[s], a[t]);
        }
    }
    return 0;
}

相似題目: hdu-1003

相關文章
相關標籤/搜索