1. 程式人生 > >瘋狂 dp(一) 線性 dp

瘋狂 dp(一) 線性 dp

     動態規劃是為了求解一種包含大量重疊子問題的最優化問題的方法。基本思想是,將原問題分解為若干相似的子問題,在求解的過程中通過子問題的解求出原問題的解。聽起來和分治法很相似,但是,分治法只是不斷地將問題分解成小問題求解,而動規之所以優秀是它會進行類似於記憶化搜尋的過程,在求解的過程中把每一個子問題的解儲存下來(不管後面會不會用到),然後在求解更大的問題時,從這些子問題的解裡面尋求當前問題的最優解。所以,從這裡也可以看出動規解法的限定條件是,該問題有一個最優子結構,就是由它分解出的子問題的解也是最優的。

    這樣看起來是很抽象的,我們接下來一個個模型看下去吧。感覺對 dp 不太好分類,個人就做簡單的整理,程式碼不對或是分類有問題請大家指出,謝謝!

    首先是線性動規。

     單調遞增最長子序列

     這個問題的基本模型是,給一個序列,要求它的最長遞增子序列,子序列的值是遞增的,它的座標在原序列中也是遞增的,但不要求連續。

     如,給一串數字, 0 2 3 4 3 4 6  9 13 24 11 26,那麼它的單調遞增最長子序列就是 0 2 3 4 6 9 13 24 26

     給一串字母,a f r e f j l m o d, 單調遞增最長子序列就是 a e f j l m o

     下面是求一個字母序列的單調遞增子序列 

     解1:

     輸入字串為 str, 用一個數組 dp 記錄問題的解, dp[i] 表示以 str[i] 結束的單調遞增最長子序列的長度。 

     狀態轉移方程: dp[i] = max(1, dp[j] + 1)  , str[j] < str[i]

     複雜度為 O(n^2)

      程式碼:    

#include <iostream>
#include <string>
#include <cstring>

using namespace std;

#define max(a,b) (a)>(b)?(a):(b)

int main()
{
	int dp[10001],ans;
	int T;
	cin>>T;
	while(T--)
	{
		ans = 1;
		string str;
		cin>>str;
		int n = str.size();
		for(int i = 0;i < n;++i)
		{
			dp[i] = 1;
			for(int j = 0;j < i;++j)
			{
				if(str[i] > str[j])
				{
					dp[i] = max(dp[i],dp[j] + 1);
					if(dp[i] > ans) ans = dp[i];
				}
			}
		}
		cout<<ans<<endl;
	}
	return 0;
}

    解二:

    陣列 dp 存不是答案子序列,但它和子序列長度是一樣的,因為它的不斷更新,所以總能確保答案正確,多看幾遍。複雜度大概只有 O(100*N),因為最長單調遞增子序列的長度是不可能超過127的。

#include<iostream>
#include<string>
#include<cstring>

using namespace std;

//求最長單調遞增子序列長度
int length(string str)
{
	int dp[128], ans = 1;;
	memset(dp,0,sizeof(dp));
	int n = str.size();
	dp[0] = str[0]; //初始化
	for(int i = 0;i < n;++i)
	{
		for(int j = ans - 1;j >= 0; --j)
		{
			if(j == 0 && dp[0] > str[i]) dp[0] = str[i];
			if(dp[j] < str[i])
			{
				dp[j+1] = str[i];
				if(j == ans - 1) ans++;
				break;
			}
		}
	}
	return ans;
}

int main()
{
	int T;
	string str;
	cin>>T;
	while(T--)
	{
		cin>>str;
		cout<<length(str)<<endl;
	}
	return 0;
}

    單調遞增最長子序列應用:

     攔截導彈:

     題意:就是給一個序列然後求它的單調遞減最長子序列的長度。

     思路:按照最長單調遞增子序列的思路,只要把遞增的比較改為遞減的比較就好,length1 函式為上面解法一的修改解法, length2 為上面解法二的修改解法,而且運用二分查詢存放位置。

     程式碼:

/*  
 *  http://acm.nyist.net/JudgeOnline/problem.php?pid=79
 *
 * 求最長單調遞減子序列長度
 */

#include<iostream>
#include<cstring>
#include <cstdio>

using namespace std;

#define MAXN 20 + 1
#define max(a,b) (a)>(b)?(a):(b)  

int num[MAXN];
int dp[MAXN];

//直接 dp ,O(n^2)
int length1(int *num,int n,int *dp)
{
	int ans = 1;
	memset(dp,0,sizeof(dp));
	for(int i = 0;i < n;++i)
	{
		dp[i] = 1;
		for(int j = 0;j < i;++j)
		{
			if(num[j] > num[i])
			{
				dp[i] = max(dp[i],dp[j] + 1);
				ans = max(ans,dp[i]);
			}
		}
	}
	return ans;
}

//求最長單調遞減子序列長度,運用二分
int length2(int *num, int n, int *dp)
{
	int ans = 1;;
	memset(dp,0,sizeof(dp));
	dp[0] = num[0]; //初始化
	for(int i = 0;i < n;++i)
	{
		//二分查詢 num[i] 應放的位置
		int st = 0,ed = ans - 1;
		while(st <= ed)
		{
			int mid = (st + ed)/2;
			if(dp[mid] == num[i]) break;
			if(dp[mid] > num[i])
			{
				st = mid + 1;
			}
			else
			{
				ed = mid - 1;
			}
		}
		// st > ed 的時候才要將 num[i] 放進去,用紙演算一下,要放在 dp[st] 的位置
		if(st > ed)
		{
			dp[st] = num[i];
			if(st == ans) ans++;
		}
	}
	return ans;
}

int main()
{
	int N,M;
	scanf("%d",&N);
	while(N--)
	{
		scanf("%d",&M);
		for(int i = 0;i < M;++i)
			scanf("%d",&num[i]);
		cout<<length1(num, M, dp)<<endl;
	}
	return 0;
}

    合唱隊形:

    題意:給 n 個整數,求一個子序列,有一箇中間數 i, 使得 i 的左邊遞增,右邊遞減,要這個子序列最長。輸出 n - length, length 為該子序列的長度。

    思路:這還是單調遞增最長子序列的問題,只是多了一步,要一個左邊遞增右邊遞減加起來最長的序列,資料較小,所以我們可以列舉。對佇列裡的每個數 i ,計算以它為中心的最長遞增和最長遞減,長度加起來和答案比較,更新 ans. 複雜度為 O(n^3) 。

    程式碼:

/* 
 * http://www.rqnoj.cn/problem/26
*/

#include <iostream>
#include <cstring>
#include <cstdio>
#include <string>

using namespace std;

#define maxn 103

int n,p[maxn],dp[maxn];
int ans;

int solve()
{
	for(int i = 1;i <= n;++i)
	{
		for(int j = 1;j <= n;++j)
		{
			dp[j] = 1;
			for(int k = 1;k < j;++k)
			{
				if(p[k] < p[j]) dp[j] = max(dp[j],dp[k]+1);
			}
		}
	    int l = dp[i];

		for(int j = n;j >= i;--j)
		{
			dp[j] = 1;
			for(int k = n;k > j;--k)
			{
				if(p[k] < p[j]) dp[j] = max(dp[j],dp[k]+1);
			}
		}
		int r = dp[i];

		ans = max(ans,l+r-1);
	}
	return n-ans;
}

int main()
{
	while(scanf("%d",&n)!=EOF)
	{
		for(int i = 1;i <= n;++i)
			scanf("%d",&p[i]);

		printf("%d\n",solve());
	}
	return 0;
}

    子串和

     這是線性 dp 的另一種型別。給出 n 個整數,求一個連續的子串,使得這個子串的和在所有子串中最大。

     思路:這道題的空間複雜度可以降到 O(1),不需要把所有的數存起來,只要一邊輸入一邊判斷就好。我們用 last 表示到前一位的最大子串和,初始化是 0, 那麼以某個數結尾的最大子串和的最壞情況就是它本身,如果 last >0,到當前數的最大子串和肯定是當前數加上 last,然後動態更新 ans 就好。

    程式碼:

/*
 * http://acm.nyist.net/JudgeOnline/problem.php?pid=44
 */

#include <iostream>
#include <cstdio>

using namespace std;

#define INF -99999999
#define max(a,b) (a)>(b)?(a):(b)

int main()
{
	int T,n,last,cur;
	scanf("%d",&T);
	while(T--)
	{
		int sum = INF;
		last = 0;
		scanf("%d",&n);
		for(int i = 0;i < n;++i)
		{
			scanf("%d",&cur);
			//這是關鍵一步,如果前面的子串和 >0,那麼這一步的最大子串和為這一個數加上前一步的最大子串和
			if(last > 0) cur += last;
			sum = max(sum,cur);
			last = cur;
		}
		printf("%d\n",sum);
	}
	return 0;
}

    01串

    題意:長度為 n 的所有 0 1 串中,不含 11 的有多少個。

    思路:dp[i] 表示長度為 i 的 0 1串不含 11 的有多少個。初始條件是 dp[1] = 2   dp[3] = 3  

                算dp[i] 的時候,如果 i 位為 0 ,那麼前 i-1 位任取,所以 dp[i] += dp[i-1] 

                如果 i 位為 1,那麼 i-1 位只能為 0,前 i-2 位任取,所以 dp[i] += dp[i-2]

                狀態方程: dp[i] = dp[i-1] + dp[i-2]

     程式碼:

/*
 * http://acm.nyist.net/JudgeOnline/problem.php?pid=252
dp[i]表示第i為不含11的01串個數
第i位為0,則前 i-1 位可任取
第i位為1,則 i-1 位必為0,前 i-2 位任取
所以 dp[i] = dp[i-1] + dp[i-2];
*/

#include<iostream>
#include<cmath>

using namespace std;

#define MAXN 42

int main()
{
	int dp[MAXN],n;
	dp[2] = 3;
	dp[3] = 5;
	for(int i = 4;i <= 40;++i)
		dp[i] = dp[i-1] + dp[i-2];
	cin>>n;
	while(n--)
	{
		int k;
		cin>>k;
		cout<<dp[k]<<endl;
	}
	return 0;
}
    Alphacode

    題意:使用一種加密方法,把 A-Z 對映為 1 - 26,那麼根據一個加密後的序列,可能有多種解密方法,比如給 124,你可以理解成 1、2、4,也可以看成 12、4,或者1 、24,所以要求求出共有多少中可能。

    思路:用 dp[i] 表示到第 i 位有多少種解密方法,則可以看到 dp[i] 和 dp[i-1] dp[i-2] 的關係。如果第 i 個數不是 0 ,那麼這個數可以作為單獨一個字母進行解碼,前面 i-1 位任取,所以 dp[i] += dp[i-1] 。如果第 i-1 位是 1 或者 i-1 位是2,i 位是小於 7 的數,那麼它們可以翻譯為一個字母解碼,前 i-2 位任取,所以 dp[i] += dp[i-2]。

   程式碼:

/*
 *http://soj.me/1001 
 */
#include <iostream>
#include <string>
#include <cstring>

using namespace std;

int main()
{
	string str;
	int ans[10000];
	int n;
	while(cin>>str&&str!="0")
	{
		memset(ans,0,sizeof(ans));
		ans[0] = 1;ans[1] = 1;
		n = str.size();
		for(int i=1;i<n;++i)
		{
			if(str[i]!='0') ans[i+1] += ans[i];
			if(str[i-1]=='1'||(str[i-1]=='2'&&str[i]<'7')) ans[i+1] += ans[i-1];
		}
		cout<<ans[n]<<endl;
	}
	return 0;
}

    

相關推薦

瘋狂 dp 線性 dp

     動態規劃是為了求解一種包含大量重疊子問題的最優化問題的方法。基本思想是,將原問題分解為若干相似的子問題,在求解的過程中通過子問題的解求出原問題的解。聽起來和分治法很相似,但是,分治法只是不斷地將問題分解成小問題求解,而動規之所以優秀是它會進行類似於記憶化搜尋的過程

【筆記篇】斜率優化dp HNOI2008玩具裝箱

公式 現在 getchar() 就是 clu cst 差距 直接 source 斜率優化dp 本來想直接肝這玩意的結果還是被忽悠著做了兩道數論現在整天渾渾噩噩無心學習甚至都不是太想頹廢是不是藥丸的表現各位要知道我就是故意要打刪除線並不是因為排版錯亂反正就是一個del標簽嘛

學習動態規劃DP——DAG模型

之前初學了一點關於動態規劃的知識,但沒有系統的學習,最近在空閒時間根據紫書(演算法競賽入門經典)開始了比較有計劃的學習,先寫下這篇部落格,作為筆記。 一、我對動態規劃的看法。 動態規劃,即是把原問題劃分為各個規模更小的問題去解決,原問題的最優解包括了

石子合併 區間dp

石子合併(一) 題目描述:     有N堆石子排成一排,每堆石子有一定的數量。現要將N堆石子併成為一堆。合併的過程只能每次將相鄰的兩堆石子堆成一堆,每次合併花費的代價為這兩堆石子的和,經過N-1次合併後成為一堆。求出總的代價最小值。 輸入描述: 有多組測試資料,輸入到

NYOJ 737 石子合併(區間dp)

石子合併(一) 時間限制:1000 ms  |  記憶體限制:65535 KB 難度:3 描述    有N堆石子排成一排,每堆石子有一定的數量。現要將N堆石子併成為一堆。合併的過程只能每次將相鄰的兩堆石子堆成一堆,每次合併花費的代價為這兩堆石子的和,經過N-1次合

好學易懂 從零開始的插頭DP

好學易懂 從零開始的插頭DP(一) 寫在前面 這是一篇,以蒟蒻視角展開的梳理總結。更改了一些順序,變化了一些細節。方便蒟蒻學習理解(起碼本蒟蒻是這樣)。大佬們可以直接看其它大佬的部落格,可以學的更快。 你必須要學會的前置知識:狀態壓縮DP學不會依舊可以讀,但是推薦學的前置知識:雜湊 論文貢前面,建議讀完部落格

機器學習理論——線性回歸

隨機 .cn 過程 小寫 找到 想想 每次 回歸 所在 (一)單變量線性回歸。 舉個例子來說,假如你要在北京的五環路租房,要預測房子的價格,其中一個比較顯著的特征就是房子的面積,根據不同的房間的面積來預測租金是多少。於是你就可以構建一個模型橫軸是房間面積,縱軸是租金

ng機器學習視頻筆記——線性回歸、代價函數、梯度下降基礎

info 而且 wid esc 二維 radi pan 圖形 clas ng機器學習視頻筆記(一) ——線性回歸、代價函數、梯度下降基礎 (轉載請附上本文鏈接——linhxx) 一、線性回歸 線性回歸是監督學習中的重要算法,其主要目的在於用一個函數表

數據結構線性表鏈式存儲實現

spl 原因 pause main -- 基本 無法 輸入 pen (一)前提 在前面的線性表順序存儲結構,最大的缺點是插入和刪除需要移動大量的元素,需要耗費較多的時間。原因:在相鄰兩個元素的存儲位置也具有鄰居關系,他們在內存中的位置是緊挨著的,中間沒有間隙,當然無法快速

數據結構線性表循環鏈表之約瑟夫環

cli amp tlist isp alloc 個人 pla 初始 ont (一)前提 41個人報數,1-3,當誰報數為3,誰就去嗝屁。現在獲取他們嗝屁的順序 (二)實現結構 順序:3->1->5->2->4 (三)代碼實現 #def

數據結構線性表循環鏈表相關補充

width hide cli 機器 都是 實時 思路 在外 for循環 (一)合並兩個循環鏈表 p = rearA->next; //A的頭結點,一會還要使用 rearA->next = rearB->next->next

數據結構線性表雙向鏈表

tro i++ crt 初始 emp 交換 strong truct erro (一)定義 雙向鏈表是在單鏈表的每個結點中,再設置一個紙箱其前驅結點的指針域 (二)結點結構 typedef struct Node { ElemType data; st

機器學習筆記線性迴歸模型

一、線性迴歸模型 (一)引入—梯度下降演算法 1. 線性假設: 2. 方差代價函式:   3. 梯度下降:   4. : learning rate (用來控制我們在梯度下降時邁出多大的步子,值較大,梯度下降就很迅速) 值過大易造成無法收斂到minimum(每一步邁更大)

大話資料結構——線性表順序儲存結構的java實現

    在看《大話資料結構》的時候,裡面詼諧的語言和講解吸引了我,但是這本書是用C來實現的,但是作為一個手擼java的人就想著用java來實現一下這些資料結構,於是就有了這些大話資料結構之java實現。哈哈,感覺這樣會讓自己的理解加深不少。 &n

機器學習:SVM——線性可分支援向量機原理與公式推導

原理 SVM基本模型是定義在特徵空間上的二分類線性分類器(可推廣為多分類),學習策略為間隔最大化,可形式化為一個求解凸二次規劃問題,也等價於正則化的合頁損失函式的最小化問題。求解演算法為序列最小最優化演算法(SMO) 當資料集線性可分時,通過硬間隔最大化,學習一個線性分類器;資料集近似線性可分時,即存在一小

資料結構線性

特徵性質   線性結構 (1)唯一一個第一,唯一一個最後 (2)除第一個外均有唯一後繼,除最後一個均有唯一前驅 分為: 順序儲存線性表 優點 查詢快       缺點 插入刪除慢  可以陣列實現   資料的擴容  動態陣列 無序陣列的應用: 排序 氣泡

Java資料結構和演算法線性結構之單鏈表

Java資料結構和演算法(一)線性結構之單鏈表 prev current next -------------- -------------- -------------- | value | next | ->

Java資料結構和演算法線性結構

Java資料結構和演算法(一)線性結構 資料結構與演算法目錄(https://www.cnblogs.com/binarylei/p/10115867.html) 線性表 是一種邏輯結構,相同資料型別的 n 個數據元素的有限序列,除第一個元素外,每個元素有且僅有個直接前驅,除最後一個元素外,每個元素

【機器學習+sklearn框架】 線性模型之Linear Regression

前言 一、原理    1.演算法含義    2.演算法特點 二、實現   1.sklearn中的線性迴歸   2.用Python自己實現演算法 三、思考(面試常問) 參考 前言        線性迴歸(Linear Regression)基本上可以說是機器

線性迴歸與特徵歸化(feature scaling)

吳恩達機器學習視訊 https://study.163.com/course/courseMain.htm?courseId=1004570029   線性迴歸是一種迴歸分析技術,迴歸分析本質上就是一個函式估計的問題(函式估計包括引數估計和非引數估計),就是找出因變數和自變數之