9/18 DQ & DP

 -- 程式競賽基礎篇 --

Introduction

 -- 課程簡介 --

自我介紹

我來講這堂課怪怪的

 

理由 1. 我好爛QQ

理由 2. 見下一頁

蕭梓宏 <3

怪怪的

這堂課是臨時調來的

 

希望能以經典題演示與實際解題為主

若備課不充分敬請多多包涵 ><

似曾相識?

對啦講義是抄來的

 

可是... 抄自己的不算抄吧QQ

而且我有改一下啦

Review

 -- 你記得多少 --

枚舉法

 -- 寧可錯殺一百,不可縱放一人 --

貪心法

 -- 貪心是人類的本性 --

二分搜尋法

 -- 看看是否有單調性 --

實作&語法

 -- 當然要會寫程式 --

懶人約定

  1. 假設沒特別說明,「數字」皆是指整數(int)

  2. 沒訂定明確下界的數(如\(N\leq10^6\)),皆是指正整數
  3. 電腦一秒鐘約可以跑\(5\times10^8\)個基本運算
  4. 區域變數約可以宣告\(2\times10^6\)bytes,全域變數則是\(4.9\times10^8\)bytes左右,題目通常限制是\(2\times10^8\)bytes
  5. 範例code不一定100%正確,WA了歡迎指正
  6. TIOJZeroJudgeCodeforcesKattisUXaPTJ

DQ

 -- 大事化小,小事化無 --

Divide & conQuer

分治法 - 將問題拆分成小問題,再一一解決

Divide:拆成相同結構的小問題
Conquer遞迴解決小問題
Combine:利用小問題的答案解決更大的問題

快速冪

求 \(a^b \bmod m\)

\(a, b\leq10^{18}, m\leq10^9\)

怎麼分

Divide

    先計算 \(a^{\lfloor\frac{b}{2}\rfloor}\bmod m\) 的值
Conquer

    遞迴解決小問題
Combine

    \(a^b \equiv (a^{\lfloor\frac{b}{2}\rfloor})^2 \pmod m\)

    如果 \(b\) 是奇數則答案要再乘上 \(a\)

複雜度?

Combine

    \(a^b \equiv (a^{\lfloor\frac{b}{2}\rfloor})^2 \pmod m\)如果 \(b\) 是奇數則答案要再乘上 \(a\)

 

\(T(n) = T(\lfloor\frac{n}{2}\rfloor) + O(1)\)

\(T(n) = O(\log n)\)

Code

#include <bits/stdc++.h>
#define int long long
using namespace std;

int power(int a, int b, int m){
    if(!b) return 1;
    int x = power(a, b>>1, m);
    if(b&1) return ((x*x)%m * a)%m;
    else return (x*x)%m;
}

int main(){
    int a, b, m;
    cin >> a >> b >> m;
    cout << power(a%m, b, m) << endl;
}

迭代式

#include <bits/stdc++.h>
#define int long long
using namespace std;

int power(int a, int b, int m, int ans = 1) {
    while(b) {
        if(b & 1) ans *= a, ans %= m;
        a *= a, a %= m, p >>= 1;
    }
    return ans;
}

int main() {
    int a, b, m;
    cin >> a >> b >> m;
    cout << power(a%m, b, m) << endl;
}

河內塔

你現在有三根柱子跟\(R\)個大小不一的圓環,一開始所有圓環都套在第一根柱子上,且較小的圓環在上,疊成一個塔。現在你要將所有圓環都移動到第三根柱子,每次你可以從任意柱子的最上方拿取一個圓環放到任意的另一根柱子上,但是在過程中大圓環不能疊在小圓環上,你有辦法將這個過程輸出嗎?

怎麼分

Divide

    拆解 - solve(x, from, to)印出將x個圓盤的塔從柱子from移到柱子to的過程
Conquer

    遞迴解決小問題
Combine

    將除了最下面的圓環塔統統移開,再將最下面的圓環移至目的地,最後再把上面的圓環塔移回去

複雜度?

Combine

    將除了最下面的圓環塔統統移開,再將最下面的圓環移至目的地,最後再把上面的圓環塔移回去

 

\(T(n) = 2T(n-1) + O(1)\)

\(T(n) = O(2^n)\)

Code

#include <bits/stdc++.h>
using namespace std;

void move(int r, int from, int to){
    if(r == 1){
        cout << from << ' ' << to << endl;
        return;
    }
    int relay = 6 - from - to;
    move(r - 1, from, relay);
    move(1, from, to);
    move(r - 1, relay, to);
}

int main(){
    int n;
    cin >> n;
    move(n, 1, 3);
}

合併排序

題敘略

怎麼分

邊界條件

    如果原序列長度是1,則不用排已有序

Divide

    將序列拆成左右兩邊
Conquer

    遞迴解決小問題 - 將左右兩邊排序
Combine

    將左右兩個排好的序列合成一個排好的序列

怎麼合

Combine將左右兩個排好的序列合成一個排好的序列

5 3 6 8 7 0 9 1 2 4

3 5 6 8 7 0 1 2 4 9

0 1 2 3 4 5 6 7 8 9

複雜度?

Conquer

    遞迴解決小問題 - 將左右兩邊排序
Combine

    將左右兩個排好的序列合成一個排好的序列

 

\(T(n) = 2T(n) + O(n)\)

\(T(n) = O(n\log n)\)

Code

#include <bits/stdc++.h>
using namespace std;

int a[500010], temp[500010];
void merge_sort(int, int);

int main(){
    int n;
    cin >> n;
    for(int i=0; i<n; i++) cin >> a[i];
    merge_sort(0, n);
    for(int i=0; i<n; i++) cout << a[i] << ' ';
    cout << endl;
}

Code

void merge_sort(int l, int r){
    if(l+1 == r) return;
    int mid = l + r >> 1;
    merge_sort(l, mid), merge_sort(mid, r);
    int lptr = l, rptr = mid, ptr = l;
    while(lptr < mid || rptr < r){
        if(lptr != mid && (rptr == r || a[lptr] < a[rptr]))
            temp[ptr++] = a[lptr++];
        else temp[ptr++] = a[rptr++];
    }
    for(int i=l; i<r; i++) a[i] = temp[i];
}

Sorting

// sorting an array with ascending order
sort(arr, arr+n);

// sorting an array with custom comparison
sort(arr, arr+n, cmp);

// sorting a vector with ascending order
sort(vec.begin(), vec.end());

// sorting a vector with custom comparison
sort(vec.begin(), vec.end());

逆序數對

給你一個數列\(a_n\),問有幾組數對\((i, j)\)滿足\(i<j, a_i>a_j\)

也就是問逆序數對的個數啦

怎麼分

邊界條件

    如果原序列長度是1,則沒有任何逆序數對

Divide

    將序列拆成左右兩邊
Conquer

    遞迴解決小問題 - 算出左右兩個區間的逆序數對數
Combine

    將左右兩邊的答案以及跨過兩邊的答案相加

怎麼合

Combine將左右兩邊的答案以及跨過兩邊的答案相加

5 3 6 8 7 0 9 1 2 4

3 5 6 8 7 0 1 2 4 9

0 1 2 3 4 5 6 7 8 9

Practice

 -- 不積跬步,無以致千里 --

逆序數對

對一個數列\(S\)來說,若\(S\)的第\(i\)項\(s_i\)與第\(j\)項\(s_j\)符合\(s_i>s_j\),並且\(i<j\)的話,那麼我們說\((i,j)\)是一個逆序數對。請問給定\(S\),總共有多少個逆序數對呢?

Thanos Sort

給一個長度\(2^N\)的數列,現在你可以對這個序列使用兩種操作:(1)將左邊\(2^{N-1}\)個數字拿掉(2)將右邊\(2^{N-1}\)個數字拿掉,每次操作結束之後數列長度都會變成一半。你可以使用這兩種操作任意多次,而使得序列變成一個遞增序列。請問這個遞增序列的長度最長是多少? \((N \leq 16)\)

河內塔改

一個有偶數層的河內塔,有\(a, b, c\)三根柱子,假設所有的環原本在\(a\)柱上,請將奇數號的環移到\(b\)柱上,偶數號的環移到\(c\)柱上,大的環不能疊在小的環上,請輸出移動過程和最少步數。

鬼鬼鬼鬼鬼腳圖的拉

有人請你構造一個鬼腳圖出來,而他會給你一些要求,例如從左數來的第一個點要走到從左數來的第三個點、左數來的第四個點要走到從左數來的第二個點...

你的任務是輸出他給定的限制最少要幾個橫槓才能完成,讓他可以挑一個最客家的方式拿到他想要的鬼腳圖。

最多有\(5\times 10^5\)個點

最近點對

給平面上\(N\)個點的座標,求距離最近的兩個點的距離到小數點第六位

最大連續和

有一個數列\(<a_i>\),總共有\(N (\leq10^6)\)個整數,求數字總和最大的一段區間的總和。(區間可以為空,空區間的總和為0)

第K大連續和

給你一個數列\(S\),一個該數列的連續和(Continuous Sum,以下簡稱CS)是指\(S\)當中的某些連續項之總和。

很容易算得出來,一個總長度為項的數列\(S\),其連續和(CS)共有 \(\frac{n(n+1)}{2}\) 個。 注意,問題來囉!

請問,這 \(\frac{n(n+1)}{2}\) 個連續和(CS)之中,第\(k\)大的是多少?

第K大連續和

提示:做好前綴和之後對答案二分搜

 

則題目就轉化為:

對於所有 \(j > i\),滿足 \(S[j] - S[i] > ans\) 的 \(i, j\) 有幾個?

是否小於\(k\)個?

Break Time

 -- 休息是為了不要走路 --

正常的題

怪題

簡單題

或初音ミク(軟體)

DP

 -- 動態規劃初步 --

動態規劃

 -- Dynamic Programming? --

display port

將問題拆解

拆成相同結構的小問題
解決小問題
紀錄小問題的最優解
利用小問題的答案解決更大的問題

-- DP = DQ + Memoization --

動態規劃是1950年代,Richard Bellman 等人在研究多階段決策過程(Multistage Decision Process)時發明的優化問題方法。但他的上司不喜歡理論的研究,所以當他發明這方法時就取了一個跟數學無關的名字,也就是現在常聽到的 Dynamic Programming。

為什麼是動態規劃

水題

 輸入一個正整數\(N\),輸出費氏數列第\(N\)項

\((N\leq 50)\)

 費氏數列是滿足以下條件的數列

\(a_1 = a_2 = 1\)

\(a_n = a_{n-1} + a_{n-2}\)    \((n \leq 3)\)

遞迴太慢了

#include <bits/stdc++.h>
using namespace std;

int fib(int n){
    if(n <= 1) return n;
    return fib(n-1) + fib(n-2);
}

int main(){
    int n;
    cin >> n;
    cout << fib(n) << endl;
}

所以我們需要DP

#include <bits/stdc++.h>
using namespace std;

int FIB[51];

int fib(int n){
    if(n <= 1) return n;
    if(FIB[n]) return FIB[n];
    return FIB[n] = fib(n-1) + fib(n-2);
}

int main(){
    int n;
    cin >> n;
    cout << fib(n) << endl;
}

DP的另一種形式

#include <bits/stdc++.h>
using namespace std;

int FIB[51];

int main(){
    int n;
    cin >> n;
    FIB[1] = FIB[2] = 1;
    for(int i=3;i<n;i++){
        FIB[i] = FIB[i-1] + FIB[i-2];
    }
    cout << FIB[n] << endl;
}

當然還可以優化

#include <bits/stdc++.h>
using namespace std;

int main(){
    int n, a, b, c;
    cin >> n;
    a = b = 1;
    for(int i=3;i<n;i++){
        c = a + b, a = b, b = c;
    }
    cout << c << endl;
}

滾動、矩陣快速冪...

DP三步驟

  1. 決定狀態

  2. 列出轉移

  3. 打好基底 (邊界)

當你覺得這題是個DP題...

DP三步驟

  1. 決定狀態dp[i] - 費氏數列第 i 項

  2. 列出轉移式:dp[i]  dp[i-1] + dp[i-2]

  3. 打好基底 (邊界):dp[1] = dp[2] = 1

以費氏數列為例

DP需要的是良好的狀態定義以及轉移順序

可以用DP的條件

  1. 最優子結構這個狀態的最優解仰賴子問題的最優解,也就是說狀態的定義時常是某狀態的最大/最小值或直接是某問題的答案。

  2. 無後效性:若求出dp[3]=2,那dp[3]永遠是2,不會因為要求不同的dp[i]而改變dp[3]的值。

  3. 重複子問題:計算dp[4]可能要用到dp[3],問題重複了,不需要重算一次dp[3]。

Classical Problems

 -- 知己知彼,百戰百勝 --

最大連續和

 -- maximum subarray sum --

有一個數列\(<a_i>\),總共有\(N (\leq10^7)\)個整數,求數字總和最大的一段區間的總和。(區間可以為空,空區間的總和為0)

比較慢的做法

1. 枚舉所有區間,逐一加起來,\(O(n^3)\)

2. 前綴求區間和,\(O(n^2)\)

3. 分治,\(O(n\log n)\)

 

感覺都會TLE,試試看\(O(n)\)DP吧!

怎麼DP

決定狀態

    dp[i] - 以第 i 個數字結尾(或空區間)的最大區間和

列出轉移式:

    dp[i] ← max(0, dp[i-1] + a[i])

打好基底 (邊界):

    dp陣列初始化成0就好了

 

這樣一來我們最後將所有dp值取max就是答案了

Code

#include <bits/stdc++.h>
using namespace std;

int a[10000010], dp[10000010];

int main(){
    int n;
    cin >> n;
    for(int i=0; i<n; i++) cin >> a[i];
    for(int i=0; i<n; i++)
        dp[i] = max(0, dp[i-1] + a[i]);
    cout << *max_element(dp, dp+n) << endl;
}

最大連續和

 -- 類題:一定得要選 --

有一個數列\(<a_i>\),總共有\(N (\leq10^7)\)個整數,求數字總和最大的一段區間的總和。(區間不可以為空)

湊零錢問題

 -- 不能Greedy怎麼辦 --

蕭電有\(N\)個硬幣,但是他的硬幣來自特殊國度,所以會有奇怪的面額,算起錢來也特別麻煩。假設第\(i\)個硬幣的面額是\(a_i\)元,而商品的價格是\(p\)元,請問蕭電能夠在不需找零的情況下購買此商品並付清嗎?\((N\leq10^4, a_i\leq10^4)\)

a = {1, 1, 4, 4, 6}

p = 8

做人不可太貪心

DP的啦

1. 決定狀態

    dp[i][j] - 只利用前 i 個硬幣能否湊出 j 元

2. 列出轉移式: 

    dp[i][j]  dp[i-1][j-a[i]] OR dp[i-1][j]

3. 打好基底 (邊界):dp[0][0] true

 

根據狀態定義,dp[N][p] 就是答案!

MLE?

1. 決定狀態

    dp[i][j] - 只利用前 i 個硬幣能否湊出 j 元

 

重要觀念 - 空間複雜度:

    \(i\leq10^4, j\leq10^4\)

    空間複雜度約 \(4\times10^8\) bytes

 

狀態佔太多空間!

滾動 - 從轉移下手

2. 列出轉移式: 

    dp[i][j]  dp[i-1][j-a[i]] OR dp[i-1][j]

 

可以發現取得dp[i]的任何一個值只要用到dp[i-1]這條陣列。我們最後只要dp[N][p],所以在計算dp[i]時,dp[i-2]以前的數值都可以忽略!

 

所以我們可以重複使用陣列,這就是滾動dp

滾動 - 從轉移下手

2. 列出轉移式: 

    dp[i][j]  dp[i-1][j-a[i]] OR dp[i-1][j]

 

我們可以重複使用陣列,這就是dp

滾動dp很簡單,直接把維度拔掉就行了。

 

    dp[j]  dp[j-a[i]] OR dp[j]

 

Code

#include <bits/stdc++.h>
using namespace std;

int a[10010], dp[10010];

int main(){
    int n, p;
    cin >> n >> p;
    dp[0] = 1;
    for(int i=1; i<=n; i++) cin >> a[i];
    for(int i=1; i<=n; i++)
        for(int j=p; j>=0; j--)
            if(j-a[i] >= 0)
                dp[j] = max(dp[j], dp[j-a[i]]);
    cout << dp[p] << endl;
}

01背包問題

 -- APCS居然跟北市賽撞題 --

蕭電有\(N\)個硬幣,假設第\(i\)個硬幣的面額是\(a_i\)元、重量是\(w_i\)克,他想要將總重量不超過\(W\)克的硬幣送給女朋友致瑄當生日禮物,請問蕭電最多能送給女朋友多少錢呢?\((N\leq10^4, a_i\leq10^4)\)

輸出解法

 -- 類題:01背包問題改 --

蕭電有\(N\)個硬幣,假設第\(i\)個硬幣的面額是\(a_i\)元、重量是\(w_i\)克,他想要將總重量不超過\(W\)克的硬幣送給女朋友致瑄當生日禮物,請問蕭電要拿哪幾個硬幣才能送給女朋友最多錢呢?\((N\leq10^3, a_i\leq10^3)\)

最長遞增子序列

 -- 就是LIS啦 --

這裡有\(N\)個人排成一列,每個人的身高都不一樣,我們假設第\(i\)個人的身高是\(h_i\)。現在你要在這列人中挑選一些人出來,而且你想要這些人之中越右邊的人身高要越高才行。在這樣的條件下,你最多可以挑選出多少人呢? \((N\leq2000, h_i\leq10^9)\)

怎麼DP

決定狀態

    dp[i] - 最右邊為左邊第 i 個人的LIS長度

列出轉移式:

    dp[i] ← \(\max\limits_{j<i, h_j<h_i}\){dp[j]} + 1

打好基底 (邊界):

    dp陣列初始化成0就好了嘍

Code

#include <bits/stdc++.h>
using namespace std;

int dp[2010], h[2010];

int main(){
    int n;
    cin >> n;
    for(int i=0; i<n; i++) cin >> h[i];
    for(int i=0; i<n; i++){
        for(int j=0; j<i; j++)
            if(h[j] < h[i]) dp[i] = max(dp[i], dp[j]);
        dp[i]++;
    }
    cout << *max_element(dp, dp+n) << endl;
}

還能再更快

 -- 還記得二分搜尋嗎 --

改變限制:\(N\leq10^5\)

這題其實有\(O(n\log n)\)的解法!

 

讓我們改變一下DP方式吧!

換一種DP方式

換一種狀態

    dp[i][j] - 前\(i\)個數字中所有長度為 \(j+1\) 的遞增子序列最右邊數字的最小值

列出轉移式:

    dp[i][j] ← \(\min\limits_{j = 0 或 dp[i-1][j-1]<a[i]}\){a[i]}

打好基底 (邊界):

    dp陣列初始化成INF (很大的數字)

你可以滾了

換一種狀態

    dp[i][j] - 前\(i\)個數字中所有長度為 \(j+1\) 的遞增子序列最右邊數字的最小值

列出轉移式:

    dp[i][j] ← \(\min\limits_{j = 0 或 dp[i-1][j-1]<a[i]}\){dp[i-1][j], a[i]}

 

明顯看到 i 這個維度可以重複利用,把它拔掉

    dp[j] ← \(\min\limits_{j = 0 或 dp[j-1]<a[i]}\){dp[j], a[i]}

一些發現

換一種狀態

    dp[j] - 前\(i\)個數字中所有長度為 \(j+1\) 的遞增子序列最右邊數字的最小值

列出轉移式:

    dp[j] ← \(\min\limits_{j = 0 或 dp[j-1]<a[i]}\){dp[j], a[i]}

 

1. 不管 i 是多少,dp[j] 隨著 j 增加而增加

2. 對於每個 i,轉移時用到 a[i] 的 j 只有一個,其餘都是從 dp[j] 轉移來

Code

二分搜從 a[i] 轉移的 j!

#include <bits/stdc++.h>
using namespce std;

int a[100010], dp[100010];

int main(){
    int n;
    cin >> n;
    for(int i=0; i<n; i++) cin >> a[i];
    fill(dp, dp+n, 1e9);
    for(int i=0; i<n; i++)
        *lower_bound(dp, dp+n, a[i]) = a[i];
    cout << lower_bound(dp, dp+n, 1e9) - dp << endl;
}

最長遞增子序列

 -- 類題:印出LIS --

這裡有\(N\)個人排成一列,每個人的身高與能力都不一樣,我們假設第\(i\)個人的身高是\(h_i\)。現在你要在這列人中挑選一些人出來,而且你想要這些人之中越右邊的人身高要越高才行。在這樣的條件下,你需要挑選出哪些人呢? \((N\leq2000, h_i\leq10^9, a_i\leq10^9)\)

一樣可以 backtracking

就不 DEMO

最長遞增子序列

 -- 類題:帶權LIS --

這裡有\(N\)個人排成一列,每個人的身高與能力都不一樣,我們假設第\(i\)個人的身高是\(h_i\)、能力是\(a_i\)。現在你要在這列人中挑選一些人出來,而且你想要這些人之中越右邊的人身高要越高才行。在這樣的條件下,你挑選出的這些人能力總和最大是多少呢? \((N\leq2000, h_i\leq10^9, a_i\leq10^9)\)

Practice

 -- 不積小流,無以成江海 --

WARNING

DP題目很多,幾乎場場必出,題型更是變化多端,讓我們來看看這一大堆的題目吧!

n!

給一個小於13的整數\(n\),輸出\(n!\)的值。

三角旅行 [IOI94]

給一個有數字構成的三角形,現在請問從最頂端走到最底端最大的和是多少。

* 每個點只能往左下或右下走,底層的點不能再往下走。
* 三角形的高度介於1到100之間。
* 三角形上的數字都介於0到99之間

Brick Wall Patterns

你要用許多\(1\times 2\)的磚頭蓋一個\(2\times n\)的磚牆,總共可以蓋出幾種花樣呢?(磚塊可以直放、橫放,磚塊不可切割且一定要鋪滿磚牆) \((n\leq 50)\)

Tri Tiling

In how many ways can you tile a \(3\times n\) rectangle with \(2\times1\) dominoes? \((n\leq 30)\)

編輯距離

兩字串A, B,你現在可以做下列三種操作:

  1. 刪除一個字元
  2. 插入一個字元
  3. 修改一個字元

請問要將字串A改成字串B最少需要多少次操作

(|A|, |B| \(\leq 1000\))

Tips: getline(cin, s);

LIS

給你長度為N的一個正整數序列,請你求出最長嚴格遞增子序列的長度。
所謂嚴格遞增子序列,是指去掉序列中的某些數字之後,剩下的子序列是嚴格遞增的。

G.N箱M球

\(n\)個相同的箱子要放入\(m\)個不同的球,總共有幾種放法?

\(5 \leq n, m \leq 200\)

俄羅斯娃娃

有\(N\)個俄羅斯娃娃,第\(i\)個娃娃的高度是\(h_i\)、寬度是\(w_i\)。如果一個娃娃的寬度與高度都小於另一個,那麼就可以將兩個娃娃嵌套。你的任務是根據這些寬度以及高度算出最多可以嵌套多少層的娃娃。\((N\leq 20000)\)

Basketball Exercise

教練想要挑幾個人組隊打籃球,他希望挑到的人身高總和越大越好。現在所有選手兩個兩個並排成兩排,每一排都是\(N\)個人。教練不喜歡選到連續的人,所以只要有一個人被選到,他的前後或旁邊的人都不能再選。現在輸入每個人的身高,求教練能選到的最大身高總和。\((N \leq 10^5)\)

貪婪之糊

給一個\(N\)個數字的序列,你每次可以選擇一個不是最左邊或最右邊的數字\(a_i\)把它消除,不過這樣需要花費 \(a_{i-1}\times a_i \times a_{i+1}\) 的代價,直到序列只剩下左右兩個數字為止。求操作完成後的最小花費。\((N \leq 50)\)

超級馬拉松賽

有\(n\)段路,每段路有一個分數\(a_i\),你每段路可以用其中一種速度
1.用走的:你不會得到任何分數
2.用跑的:你會得到\(a_i\)的分數
3.用衝的:你會得到\(2a_i\)的分數,但你下一段路得用走的
請問你最多能得到多少分? \((n \leq 50)\)

Fire

火場裡有\(N\)個人\((N \leq 100)\),每個人因為被困在不同地方所以有不同救援難度,假設拯救第\(i\)個人所需的時間是\(t_i\)。因為火勢的蔓延,第\(i\)個人要趕在時間點\(d_i\)以前被救出來。由於救援隊私心的緣故,他們偷偷將每個人賦予權重\(p_i\),並且希望被拯救的那些人權重和越大越好。你能知道哪些人終會得救嗎?\((t_i \leq 20, d_i \leq 2000, p_i\leq 20)\)

Blocks

有一排\(n\)個方塊,每個方塊都有顏色,每次可以把連續顏色的一段消掉,若消去的方塊有\(L\)個,則可以得到\(L^2\)的分數,請問全部消完最多可以得到幾分?\((n \leq 200)\)

放錯的信封

有天爸爸交代小明幫忙把寫好的信裝進信封裡,信與信封上的名字要配對。例如:給王大毛的信要裝到寫有王大毛的信封。這時頑皮的小明想到一個惡作劇,就是把所有人的信與信封都裝錯;也就是說沒有一個人會收到正確寄給自己的信。例如:A收到B的信,B收到C的信,C收到A的信。

請幫小明算算,到底有多少種裝法可以不讓任何人收到應該寄給自己的信。(最多20人)

搜集寶藏 (Treasure)

有兩個人在\(N\times M\)的格子裡找寶藏,每個格子可能是寶藏、障礙物或空地。兩個人都由左上角進,右下角出,且每次只能往右或往下。若兩人可以分開行動,且一個寶藏只能拿一次,問兩人總共最多可以拿到多少寶藏。\((N, M \leq 100)\)

Bubble Sort Graph

給你一個氣泡排序的code以及一個\(N\)個數的序列,你現在有\(N\)個點但沒有邊的圖,當氣泡排序執行中兩數每進行一次交換,就將編號是這兩數的頂點連一條邊。求排序完成後此圖的最大點獨立集大小。\(N\leq 10^5\)

撿石子遊戲

有三個人(P, E, C)走在一條筆直的路徑上,路徑上依序有\(N\)顆石塊排成一排\((N \leq 2\times 10^6)\),且每一顆都只能被特定其中一個人撿起。之後P、E、C會各選一個區間將區間內所有可以被他撿起的石塊都拿起來。他們所選定的區間兩兩交集必須要是空的。

說石持那十塊,P、E、C三個人已經走完這條路並且撿好石塊了。請計算他們三人所持有的石塊個數總和最大是多少。

Beautiful Array

給一個\(N\)個整數的序列以及一個整數\(X\) \((N\leq 3\times 10^5, |X| \leq 100)\),現在你有一次機會可以將某段區間(可以為空)的所有數字乘上\(X\),乘完之後你可以再選擇一個區間(可以為空),這個區間的總和就是序列的美妙程度。求美妙程度最大可能值。

最長回文子序列

給一個字串\(S\),求其中一個最長的回文子序列,如果長度超過1000就只要輸出長度為1000的回文子序列即可\((|S| \leq 200000)\)

這題時限卡很緊,空間也卡很緊

最長公共子序列

給兩個字串A, B,請輸出這兩個字串的其中一個最長公共子序列 (|A|, |B| \(\leq 10000\))

這題時限跟空間又卡更緊了

還有更多更多

Relax

 -- 潤滑一下生鏽的腦 --

1. ヨルシカ

2. YOASOBI

3. THE BINARY

4. Eve

5. まふまふ

6. 美波

7. ずっと真夜中でいいのに。

Advanced DP

 -- 進階DP技巧 --

狀態壓縮DP

 -- 旅行銷售員問題 --

給定一張有向帶權圖,求權重和最小的漢米爾頓迴圈(Hamiltonian Cycle)

狀態壓縮DP

不妨設 \(dp[S][v]\) 為從

"已經訪問過\(S\)裡面所有點,且現在位於\(v\)點上"

這個狀態經過剩下所有頂點回到頂點\(0\)的最小權重和。

 

那麼答案就是 \(dp[空集合][0]\)

決定狀態

狀態壓縮DP

對於所有不在\(S\)裡面的點\(u\),都可以走過去

然後看看走到哪裡會比較划算

 

\(dp[S][v] = \min\{dp[S \cup \{u\}][u] + cost(v, u)\}\)

列出轉移式:

狀態壓縮DP

若經過所有點之後又回到\(0\),則權重和是\(0\)

\(dp[全][0] = 0\)

 

剩下的狀態就用很小的數字(e.g. -1e18)等待更新

打好基底

怎麼實作

二進制表示法:用整數表達集合的方法

 

例如有

{1, 2, 5, 6} = \(2^1 + 2^2 + 2^5 + 2^6\)

{0, 1, 4, 7, 6} = \(2^0 + 2^1 + 2^4 + 2^7 + 2^6\)

 

如此一來每一個整數都對應到一個集合

怎麼實作

二進制表示法的性質:

  • 只有元素 \(i\) 的集合:(1<<i)
  • 空集合:0
  • 包含 \(0\) 到 \(2^N-1\) 所有元素的集合:(1<<n) - 1
  • 查詢集合 \(S\) 中是否有 \(i\):if(S >> i & 1)
  • \(S\) 與元素 \(i\) 的聯集:S | 1<<i
  • \(S\) 與元素 \(i\) 的差集:S & ~(1<<i)
  • 集合的交集、聯集:S & T、S | T

怎麼實作

好用的暴力枚舉技巧 有點離題了QQ

for(int i=0;i<(1<<n);i++) {
    // 枚舉所有集合
}
int sub = sup;
do {
    // 枚舉sup的所有子集
    sub = (sub - 1) & sup;
} while(sub != sup);
int tmp = (1 << k) - 1;
while(tmp < (1 << n)) {
    // 枚舉大小為k的集合
    int x = tmp & -tmp, y = tmp + x;
    tmp = ((tmp & ~y) / x >> 1) | y;
}

回到題目

#include<bits/stdc++.h>
using namespace std;

int dp[1 << 16][16];

signed main() {
  for(int S=0;S<(1<<n);S++) fill(dp[S], dp[S] + n, -1e18);
  dp[(1 << n) - 1][0] = 0; // 目標
  for(int S=(1<<n)-1-1;S>=0;S--) {
    for(int v=0;v<n;v++) for(int u=0;u<n;u++) {
      if(!(S >> u & 1)) {
        dp[S][v] = min(dp[S][v], dp[S | 1 << u][u] + d[v][u]);
      }
    }
  }
  cout << dp[0][0] << endl;
}

狀態壓縮DP

大家應該都看過題目了吧

如果大家都AC了就跳過吧XD

subtask 做法

subtask 做法

\(i\)

01001 = 9

枚舉一整列的所有開關情形相當於枚舉 \(0\) 到 \(2^N-1\) 的所有整數

11010 = 26

\(i+1\)

subtask 做法

#include<bits/stdc++.h>
using namespace std;

int dp[16][1 << 16], ans;

signed main() {
    for(int msk=0;msk<(1 << n);msk++) dp[0][msk] = 1;
    for(int i=1;i<n;i++) {
        for(int msk=0;msk<(1 << n);msk++) {
           for(int pre=0;pre<(1 << n);pre++) {
               if(valid(msk, pre)) dp[i][msk] += dp[i-1][pre];
           }
        }
    }
    for(int msk=0;msk<(1 << n);msk++) ans += dp[n-1][msk];
    cout << ans << endl;
}

輪廓線DP做法

\(dp[i][msk]\) = 只考慮第\(i\)列以上的窗戶,且第\(i\)列的開關狀態剛好是\(msk\)的方法數

 

如果改成一格一格做

\(dp[i][j][msk]\) = ?

輪廓線DP做法

沒錯就是這樣 可以發現利用計算好的所有\(dp[i][j][pre]\)很好計算\(dp[i][j+1][msk]\)

會MLE,所以至少滾動一個維度

矩陣冪DP

 -- 費氏數列 --

a_n = a_{n-1} + a_{n-2}
\begin{bmatrix} a_n \\ a_{n-1} \end{bmatrix} = \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix} \begin{bmatrix} a_{n-1} \\ a_{n-2} \end{bmatrix}

塞到矩陣裡

變成矩陣冪了

a_n = a_{n-1} + a_{n-2}
\begin{bmatrix} a_n \\ a_{n-1} \end{bmatrix} = \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix} \begin{bmatrix} a_{n-1} \\ a_{n-2} \end{bmatrix}
\begin{bmatrix} a_n \\ a_{n-1} \end{bmatrix} = \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}^{n-1} \begin{bmatrix} a_{1} \\ a_{0} \end{bmatrix}

Practice

 -- 無他,惟手熟爾 --

費氏數列

題敘略

打地鼠

地鼠基地是一個長型的基座,基座上每隔一公尺就會有一個地鼠洞。玩家站在這個基地的最左邊,與第一個地鼠洞相距一公尺。

已知由左而右編號為\(i\)的地鼠洞每\(T_i\)秒地鼠會出現一次,且玩家移動一公尺需要一秒鐘。若被打中的地鼠便不再出現,求將所有地鼠打完所需的最少秒數。

提示:洞不多,最多16個

排教室問題

某校有\(M\)種不同的課程,其中有些課程的時間會有衝堂。如果給定每組有衝堂的課程,且知道學校中總共有\(N\)間不同的教室,請問共有多少種安排各課程上課教室的方式?最少要用到幾間教室?\((N, M \leq 10)\)

提示:我看不懂題目

索拉數列

如果說一個數列s符合
\(s_0=a\)
\(s_1=b\)
\(s_n=xa_{n-2}+ya_{n-1} (2\leq n)\)
那我們就稱這個數列為索拉數列(sola sequence)

求索拉數列第\(n\)項

其中\(n\)不會大於\(1,000,000,000\)
而\(a, b, x, y\)則會小於\(2^{32}\)

3.熱鍋上的螞蟻

螞蟻在一張由鍋子跟筷子組成的無向圖上走,且走每條邊的機率都一樣。已知螞蟻從每個鍋子當作起點的機率都一樣,求螞蟻走了\(T\)步後停在第\(X\)個鍋子上的機率是多少。

 

鍋子數\(N\leq100\),\(T\leq 10^9\)。

小鎮DP

一個正方形的鎮區分為 \(N\times N\) 個小方塊。農場位於方格的左上角,集市位於左下角。小優穿過小鎮,從左上角走到左下角,剛好經過每個方格一次。他想要知道有多少走法。\((N \leq 10)\)

大根蘿蔔

有 \(N\times N\) 的農田,每塊農田種有不同價值的蘿蔔。但是相鄰八格的蘿蔔無法一起被拔起,求能夠拔起的最大價值總和。\((N \leq 22)\)

Furthermore

-- 理無專在 而學無止境也 --

操作分治

  • 三維偏序
  • 四維偏序
  • 高維偏序

-- 就是 Merge Sort 啦 --

數學分治方法

-- 數學家與你的肝 --

  • 矩陣乘法 - Strassen's Algorithm
  • 整數乘法 - Karatsuba's Algorithm
  • 快速傅立葉轉換

樹分治 & 樹DP

-- 樹上問題 --

  • 點分治
  • 重心剖分
  • 樹鍊剖分
  • 樹上路徑問題

DP優化

  • 分治優化
  • 單調隊列優化
  • 斜率優化
  • 四邊形不等式優化
  • WQS二分搜

-- 你以為DP只是DP嗎 --

Contest

-- 小小水題賽 --

Contest

Next

-- 很毒瘤但很重要的資料結構 --

精神支柱

以前的校內培訓講義

存在TIOJ上的code們

去年的資訊讀書會講義

軟體們的歌A, B, C, D, E

osu!

DP & DQ

By 林尚廷

DP & DQ

動態規劃入門、分而治之初探

  • 2,022