1. 程式人生 > >NOIP演算法總結——關於簡單 線性動態規劃

NOIP演算法總結——關於簡單 線性動態規劃

動態規劃,顯然是一個很讓人頭疼的地方,也沒有個固定的演算法,最多就是有一些模板(比如揹包啊),要是想要增大做出來的機率,也就只好多做做題找找感覺了~

線性動態規劃可以說是DP中最簡單的型別了,當然裡面很多也是不容易的題目,也是需要一點技巧了。搞了好幾天了,現在就來總結一下。

一、演算法的選取,動態維護。

例題1:最大子段和

題目描述

給出一段序列,選出其中連續且非空的一段使得這段和最大。

輸入輸出格式

輸入格式:

輸入檔案maxsum1.in的第一行是一個正整數N,表示了序列的長度。
第2行包含N個絕對值不大於10000的整數A[i],描述了這段序列。

輸出格式:

輸入檔案maxsum1.out僅包括1個整數,為最大的子段和是多少。子段的最小長度為1。

輸入輸出樣例

輸入樣例#1:

7
2 -4 3 -1 2 -4 3

輸出樣例#1:

4

說明

【資料規模與約定】
對於40%的資料,有N ≤ 2000。
對於100%的資料,有N ≤ 200000。
【洛谷1115】

演算法一:暴力列舉。列舉每一個子段,求和,再求最大。
時間複雜度:列舉是O(n),每次計算是O(n),總共是O(n^3)。

#include <bits/stdc++.h>
using namespace std ;
int a[200010] ;
int main() {
    int i, j, k, m, n ;
    scanf ( "%d"
, &n ) ; for ( i = 1 ; i <= n ; i ++ ) scanf ( "%d", a+i ) ; int ans = -2147483647 ; for ( i = 1 ; i <= n ; i ++ ) for ( j = i ; j <= n ; j ++ ) { int sum = 0 ; for ( k = i ; k <= j ; k ++ ) sum += a[k] ; ans = max ( ans, sum ) ; } printf
( "%d\n", ans ) ; return 0 ; }

顯然不可取。
看那範圍,顯然是TLE到乾乾淨淨。

演算法二:字首和優化。和演算法一類似,也是列舉每個子段,只是子段求和的時候使用字首和優化(O(1))。
時間複雜度:O(n^2)

#include <bits/stdc++.h>
using namespace std ;
int a[200010] ;
int main() {
    int i, j, k, m, n ;
    scanf ( "%d", &n ) ;
    for ( i = 1 ; i <= n ; i ++ ) { 
        scanf ( "%d", a+i ) ;
        a[i] += a[i-1] ;
    } 
    int ans = -2147483647 ;
    for ( i = 1 ; i <= n ; i ++ ) 
        for ( j = i ; j <= n ; j ++ ) {
            int sum = 0 ;
            sum = a[j] - a[i-1] ;
            ans = max ( ans, sum ) ;
        }
    printf ( "%d\n", ans ) ;
    return 0 ;
}

啊!騙到了40分,還是可以的。
不過,顯然不是正解。

演算法三:動態維護最小字首和。
時間複雜度: O(n)
這還是要分析一下的……
對於i~j的子段,sum = a[j] - a[i-1] ;
那麼,以j為結尾的所有子段,都是a[j]減去一個什麼什麼東西。
顯然,減去的那個東西越小越好~
那麼,我們只要維護最小的那個就好了。所以時間降到了線性。

#include <bits/stdc++.h>
using namespace std ;
int a[200010] ;
int main() {
    int i, j, k, m, n ;
    scanf ( "%d", &n ) ;
    for ( i = 1 ; i <= n ; i ++ ) { 
        scanf ( "%d", a+i ) ;
        a[i] += a[i-1] ;
    } 
    int ans = -2147483647, Min = 0 ;
    for ( i = 1 ; i <= n ; i ++ ) {
        ans = max ( ans, a[i] - Min ) ;
        Min = min ( a[i], Min ) ;
    }
    printf ( "%d\n", ans ) ;
    return 0 ;
}

沒話說,這還不AC?

二、字首和的利用

就像上面那個題目裡所使用的一樣,字首和經常能給我們很大的幫助。
那是一維的‘字首和’,其實,對於一些二維甚至三維的統計(不一定是求和,也可能是求 異或 之類),我們也可使用字首和。

最大正方形【洛谷1387】

題目描述

在一個n*m的只包含0和1的矩陣裡找出一個不包含0的最大正方形,輸出邊長。

輸入輸出格式

輸入格式:

輸入檔案第一行為兩個整數n,m(1<=n,m<=100),接下來n行,每行m個數字,用空格隔開,0或1.

輸出格式:

一個整數,最大正方形的邊長

輸入輸出樣例

輸入樣例#1:

4 4
0 1 1 1
1 1 1 0
0 1 1 0
1 1 0 1

輸出樣例#1:

2

分析:如果一個矩形的元素和與面積相等,那麼它裡面的元素就全部是1

演算法一:列舉
列舉正方形的某一個定點座標(O(n×m)),在列舉正方形邊長(O(max(n,m))),再累加(O(n×m))
時間複雜度:O(n^5)

顯然會TLE的乾乾淨淨。
由於這個暴力列舉很簡單,就不貼出程式碼了。

演算法二:還是列舉,只是加字首和優化
方法同上,但是求和的過程不是累加,而是使用字首和(O(1))。

怎麼使用字首和呢?
sum(i,j) 表示點(1,1)和點(i,j)圍成矩形裡元素的和。(如下圖)
sum[i][j]

那麼,為什麼要這樣表示呢?那是因為簡便計算矩陣和啊!
求和
看上圖,假設我們要求計算的矩形(注意,這裡是可以推廣到一般情況的,矩形!)是圖示紅色方框所包括的部分(x1,y1)——(x2,y2)。
那麼,這個答案可以通過圖示 褐色方框 - 兩個青色方框 + 黑色方框(由於多減了一次) 得到。
所以,

s=sum[x2][y2]sum[x2][y11]sum[x11][y2]+sum[x11][y11]
這就是我們的計算式。
但是,怎麼計算sum[][]呢?
sum[i][j]=sum[i1][j]+sum[i][j1]sum[i1][j1]+a[i][j]

接下來的任務就很簡單了,直接上程式碼

#include <bits/stdc++.h>
using namespace std ;
const int maxn = 110 ;
bool a[maxn][maxn] ; // 記錄原始陣列
int sum[maxn][maxn] ; // sum意義同上所述
int main() {
    int i, j, k, m, n, l ;
    scanf ( "%d%d", &n, &m ) ;
    for ( i = 1 ; i <= n ; i ++ )
        for ( j = 1 ; j <= m ; j ++ ) 
            scanf ( "%d", &a[i][j] ) ;
    for ( i = 1 ; i <= n ; i ++ ) 
        for ( j = 1 ; j <= m ; j ++ ) 
            sum[i][j] = sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1] + a[i][j] ; // 遞推計算sum
    int x2, y2, x1, y1 ;
    int ans = -1000000000, L ; // 用於儲存最大面積,答案邊長
    for ( x1 = 1 ; x1 <= n ; x1 ++ ) 
        for ( y1 = 1 ; y1 <= m ; y1 ++ ) 
            for ( l = 1 ; l+x1-1 <= n && l+y1-1 <= m ; l ++ ){
                x2 = l+x1-1 ; y2 = l+y1-1 ;
                int s = sum[x2][y2] - sum[x2][y1-1] - sum[x1-1][y2] + sum[x1-1][y1-1] ;
                if ( s != l*l ) continue ;
                if ( s > ans ) {
                    ans = s ;
                    L = l ;
                }
            }
    printf ( "%d\n", L ) ;
    return 0 ;
}

是不是顯然很簡單呢~

三、簡單的遞推。

例題1:過河【NOIp 2005 T2】

題目描述

在河上有一座獨木橋,一隻青蛙想沿著獨木橋從河的一側跳到另一側。在橋上有一些石子,青蛙很討厭踩在這些石子上。由於橋的長度和青蛙一次跳過的距離都是正整數,我們可以把獨木橋上青蛙可能到達的點看成數軸上的一串整點:0,1,……,L(其中L是橋的長度)。座標為0的點表示橋的起點,座標為L的點表示橋的終點。青蛙從橋的起點開始,不停的向終點方向跳躍。一次跳躍的距離是S到T之間的任意正整數(包括S,T)。當青蛙跳到或跳過座標為L的點時,就算青蛙已經跳出了獨木橋。
題目給出獨木橋的長度L,青蛙跳躍的距離範圍S,T,橋上石子的位置。你的任務是確定青蛙要想過河,最少需要踩到的石子數。

輸入輸出格式

輸入格式:

輸入檔案river.in的第一行有一個正整數L(1 <= L <= 10^9),表示獨木橋的長度。第二行有三個正整數S,T,M,分別表示青蛙一次跳躍的最小距離,最大距離,及橋上石子的個數,其中1 <= S <= T <= 10,1 <= M <= 100。第三行有M個不同的正整數分別表示這M個石子在數軸上的位置(資料保證橋的起點和終點處沒有石子)。所有相鄰的整數之間用一個空格隔開。

輸出格式:

輸出檔案river.out只包括一個整數,表示青蛙過河最少需要踩到的石子數。

輸入輸出樣例

輸入樣例#1:

10
2 3 5
2 3 5 6 7

輸出樣例#1:

2
說明
對於30%的資料,L <= 10000;
對於全部的資料,L <= 109。

分析:

To Be Continue … …
低價購買
尼克的任務
多米諾骨牌
合唱隊形
奇怪的字串