1. 程式人生 > >平行四邊形不等式優化DP

平行四邊形不等式優化DP

一.前言

DP一直是程式設計中的一個難題,解決它不僅需要大量刷題,還需要學會各種DP的方法。這裡,我就主要講一個DP的優化方法:平行四邊形不等式優化DP動態規劃。(好難呀)

 


 

二.平行四邊形不等式是個啥?

1.題目引入:猴子派對

遠離我們的世界,有一個香蕉森林。許多可愛的猴子住在那裡。有一天,作為香蕉林之王的SDH(宋大侯)決定舉辦一個盛大的聚會慶祝瘋狂香蕉日。但小猴子彼此不認識,所以作為國王,SDH必須做點什麼。 
現在有n只猴子坐成一圈,每隻猴子都有交朋友的時間。而且,每隻猴子都有兩個鄰居。SDH希望將它們介紹給對方,規則是:1。每次 ,他只能介紹一隻猴子和一隻猴子的鄰居。 
2.如果他介紹A和B,那麼每隻已經知道的每隻猴子A都知道每隻猴子B已經知道了,這次引入的總時間是所有猴子A和B已經知道的交友時間的總和; 
3.每個小猴子都認識自己; 
為了開始聚會並儘快吃香蕉,SDH想知道他需要的最短時間。 

輸入

有幾個測試用例。在每種情況下,第一行是n(1≤n≤1000),這是猴子的數量。下一行包含n個正整數(小於1000),表示交朋友時間(按順序,第一個和最後一個是鄰居)。輸入是檔案結尾。

輸出

對於每種情況,您應該列印一行,給出SDH引入時的最小時間。

樣例輸入

8

5 2 4 7 6 1 3 9

樣例輸出

105

2.找出狀態轉移方程式

大家先不管圍成一個圈,先把它們看作一條直線。這道題的狀態轉移方程式很容易想到:就是在i~j號猴子的範圍內,任取一個猴子k,當成是把這個猴子左邊的那一堆猴子(i~k)介紹給它右邊的那一堆猴子(k+1~j),先預處理計算出i~j號猴子交朋友的總時間w[i][j],於是,狀態轉移方程式就出來了:dp[i][j] = min(dp[i][k] + dp[k + 1][j])+w[i][j ]

3.探索此狀態轉移方程式的性質

這可能有點難得想,大家一定要注意!

以現在的狀態轉移方程式來看是個三重迴圈,肯定要超時,不妨讓我們來優化一下這個k。k的值肯定無法在每次直接確定,那麼我們就縮小它的範圍。

首先,我們來探索一下w[i][j]的性質:有i<{i}'<j<{j}',那麼w[{i}'][j] <= w[i][{j}']是毋庸置疑的。還有一個性質:w[i][j] + w[{i}'][{j}'] <= w[i][j']+w[i'][j],為什麼呢?這兩個加起來不應該是一樣的嗎?現在我們用平行四邊形來理解一下:

是不是就是這個道理?所以這就是著名的平行四邊形不等式。

好,現在找出了w[i][j]的規律,那麼如何是k的規律呢?這裡有一個專有名詞:最佳決策點(k),現在因為用k表示最佳決策點的話不好搞,所以我們用s[i][j]表示dp[i][j]的最佳決策點。因為w[i][j]滿足平行四邊形不等式,所以s[i][j]也會滿足平行四邊形不等式(證明過程大家自己想),則有:s[i][j-1]<=s[i][j]<=s[i+1][j]

。我來解釋一下為什麼:①s[i][j-1]<=s[i][j],意味著在右邊少一個猴子。因為我們希望分成的左右兩堆猴子基本均勻,這樣交友總時間加起來會越少(大家一定想的出來為什麼),所以s[i][j-1]的關鍵決策點絕不會在s[i][j]的右邊;②s[i][j]<=s[i+1][j],同理,左邊少了一個猴子,關鍵決策點s[i+1][j]絕不會在s[i][j]的左邊。

如此,我們就找出了dp[i][j]的關鍵決策點的範圍了,時間就大大縮小了。

4.細節處理

只會個思路什麼都不是,現在來想一想程式碼細節問題:

1.環

這個問題很好處理,共有兩種方法:

①擷取法

因為我們的思路常常是破環成鏈,所以我們可以在整個環中每一個位置都破一遍,這有點麻煩。

②補全法

如果我們直接把整個環看成鏈是會少考慮情況的,不妨在整個鏈的後面接一個長度為(n-1)的鏈,這樣就能把所有環的情況考慮進去了。

個人推薦用補全法。

2.迴圈順序問題

因為我們關鍵決策點的範圍是s[i][j-1]<=s[i][j]<=s[i+1][j],所以我們要保證s[i][j-1]和s[i+1][j]都已經求出來了,所以i從大到小迴圈,j從小到大迴圈。

求w[i][j]的方法就不用說了吧,直接用字首和相減就行了。

5.更清晰的思維

有點懵,不要慌,大家來看一下這道題的思維圖,也許就思路清晰了許多,對平行四邊形不等式優化DP有更深刻的意識。

6.程式碼

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
#define M 2005
#define INF 0x3f3f3f3f
#define min(a, b) a < b ? a : b
int n, Time[M], dp[M][M], s[M][M], sum[M], ans;
inline void Read (int &x){
	int f = 1; x = 0; char c = getchar();
	while (c > '9' || c < '0') {if (c == '-') f = -1; c = getchar();}
	while (c >= '0' && c <= '9') {x = x * 10 + c - 48; c = getchar();}
	x *= f;
}	
int main (){
    while (~scanf ("%d", &n)){
        ans = INF;
        for (register int i = 1; i <= n; i ++){
            Read (Time[i]);
            sum[i] = sum[i - 1] + Time[i];
        }
        for (register int i = n + 1; i < 2 * n; i ++){//sum[i]是字首和,我沒有求w[i][j]
            Time[i] = Time[i - n];
            sum[i] = sum[i - 1] + Time[i];
        }
        n = n * 2 - 1;
        for (register int i = 0; i <= n; i ++)//預處理
			for (register int j = 0; j <= n; j ++)
				dp[i][j] = INF, s[i][j] = 0;
        dp[0][0] = 0;
        for (register int i = n; i >= 1; i --){
            dp[i][i] = 0;
            for (register int j = i + 1; j <= n; j ++){
                if (!s[i][j - 1])//特殊情況,特殊處理,處理成極限值
                    s[i][j - 1] = min (i + 1, j - 1);
                if (!s[i + 1][j])
                    s[i + 1][j] = j - 1;
                for (register int k = s[i][j - 1]; k <= s[i + 1][j]; k ++){
                    if (dp[i][j] > dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1]){//sum[j] - sum[i - 1]即w[i][j]
                        dp[i][j] = dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1];
                        s[i][j] = k;
                    }
                }
            }
        }
		n = (n + 1) / 2;
        for (register int i = 1; i <= n; i ++)//找答案
            ans = min (ans, dp[i][i + n - 1]);
        printf ("%d\n", ans);
    }
    return 0;
}

通過這道題,相信大家對平行四邊形不等式優化DP有了新的認識,下面來看一道變式。

7.變式

1.題目:樹的建造

對於所有i<j,考慮一個滿足Xi<Xj和Yi>Yj的點集合(Xi,Yi)的二維空間。我們希望它們都通過一個有向樹連線,樹的邊緣向右(X正)或向上(Y正)。下圖顯示了一個示例樹。


編寫一個程式,該程式將所有給定的點與最短的邊的總長度連線起來。

輸入

輸入以包含整數N(1<=N<=1000)的行開始,即點數。然後N行跟隨。第i行包含兩個整數XI和y(0<=Xi,y<=10000),它們給出了第i點的座標。

輸出

在一行中列印邊的總長度。

樣例輸入

5
1 5
2 4
3 3
4 2
5 1
1
10000 0

樣例輸出

12
0

2.思路

這道題就有點難了,每一個點是一組座標。但是可以像上一道題一樣解決,並且還沒有環,就把每個點看成每個猴子就行了。可以想到,連線i,j點的最小花費是x_{j}-x_{i} + y_{i}-y_{j},如圖:

於是,先把狀態轉移方程式寫出來:dp[i][j] = dp[i][k] + dp[k + 1][j] + a[k]_{y} - a[j]_{y} + a[k + 1]_{x} - a[i]_{x}。相信大家一定是後半部分看不懂吧。大家也許想問為何不是a[i]_{y} - a[j]_{y}+a[j]_{x}-a[i]_{x}。因為這樣會加重複,dp[i][k]和dp[k+1][j]內已經有了a[i]_{y}-a[k]_{y}a[j]_{x}-a[k+1]_{x}了,如果你想以上說的這麼加,就會把a[i]_{y}-a[k]_{y}a[j]_{x}-a[k+1]_{x}又重新加一遍。

可以藉藉助一個圖來理解:

綜上,我們的狀態轉移方程式就出來了,再利用平行四邊形的性質進行優化就行了。

3.樣例程式碼 

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
#define M 1005
#define INF 0x3f3f3f3f
struct node {
    int x, y;
}a[M];
int n, dp[M][M], s[M][M];
int main (){
    while (~scanf ("%d", &n)){
        for (int i = 1; i <= n; i ++)
            scanf ("%d %d", &a[i].x, &a[i].y);
        memset (dp, INF, sizeof(dp));
        memset (s, 0, sizeof(s));
        dp[0][0] = 0;
        for (int i = n; i >= 1; i --){
            dp[i][i] = 0;
            for (int j = 1 + i; j <= n; j ++){
                if (!s[i][j - 1])
                    s[i][j - 1] = min (i + 1, j - 1);
                if (!s[i + 1][j])
                    s[i + 1][j] = j - 1;
                for (int k = s[i][j - 1]; k <= s[i + 1][j]; k ++){
                    if (dp[i][j] > dp[i][k] + dp[k + 1][j] + a[k].y - a[j].y + a[k + 1].x - a[i].x){
                        dp[i][j] = dp[i][k] + dp[k + 1][j] + a[k].y - a[j].y + a[k + 1].x - a[i].x;
                        s[i][j] = k;
                    }
                }
            }
        }
        printf ("%d\n", dp[1][n]);
    }
    return 0;
}

三.總結

相信通過這兩道題,大家對平行四邊形不等式有了初步的概念,後面還會附上更難的題目,加油!