1. 程式人生 > >動態規劃系列(一) 01揹包問題及一維陣列優化

動態規劃系列(一) 01揹包問題及一維陣列優化

程式碼是前幾周就寫好的, 但是腦子抽了, 導致我再看時不知道是為什麼, 於是乎在CDSN上整理一下..

我是愛C++和演算法的喵線童鞋 //才沒有給自己洗腦 ⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄

=============================================

首先是最簡單的二維版本

遞迴式:  dp[i][w]=max{dp[i-1][w],dp[i-1][w-wi]+vi}

i表示第i件物品, w表示當前的重量.

這個遞迴式是怎麼來的呢?

基於我們對動態規劃的基本的瞭解, 我們知道: 動態規劃問題一般具有最優子結構性質.

假設某個0/1揹包問題給了n件物品, 總重量不超過Tw,

我們觀察其中一個子問題: 有i件物品, 總重量不超過w

(i介於第1件物品到第n件物品之間; w介於0-Tw)之間.

這裡要注意,輸入物品陣列從0下標開始的話, i的迴圈也要對應從0到n-1 (這是一個基礎而又細節的問題=.=)

這裡多說幾句.(畢竟dp是一類問題, 如果能更深入地理解, 有助於以後解決非典型問題. 當然如果已經對"遞迴"有了自己深入而清晰的理解,當然可以跳過啦~~)

思考一個問題: 為什麼會想到"遞迴"?

我自己對遞迴的理解是: 抽象為有限個層次之間的關係(比如第i-1與第i層), 而不必關心整體的實現過程. 如果這個不好理解, 就想一想我們已經熟悉的數學歸納法, 其思想原理是非常類似的. 我們只關心n=k-1到n=k之間是怎麼證明的, 而不會說把每一步都證明出來

.

至於我為什麼一直會這麼糾結為什麼遞迴, 是因為我曾經在青蛙跳臺階上的題目跪了 (手動微笑:::::) 暫時的不成功使人成長, that's quite right.

因此, 我們對於這個子問題dp[i][w], 只需考慮兩種情況: 放第i個物品和不放第i個物品. (即假設前i-1個問題都已經求解, 只關心第i-1層與第i層之間的關係,是不是很像數學歸納法~) =.=其實數歸本身也是一種遞迴恩

如果不放, 這把第i個物品扔掉, 考慮dp[i-1][w];

如果放, 就是dp[i-1][w-wi]+vi; 也就是,把wi的空間騰出來給第i個物品.

比較哪個大就好啦>.<

當然, 為了問題的完備性, 最後我們還要考慮幾種特殊情況:

1)不放物品時, dp[i][w]=0;

2)如果對於子問題的上界w, wi已經超過了w, 那麼就直接不需要考慮這個物品了, 因此此時 dp[i][w]=dp[i-1][w];

3)一般的情況, 就是上面講的dp[i][w]=max{dp[i-1][w],dp[i-1][w-wi]+vi}

因此, 只要寫個雙重迴圈就可以了: for i=0 to n-1 {for j=0 to Tw)

程式碼如下:

#include<iostream>
#include<vector>
using namespace std;
struct KNAP
{
	int w; //weight;
	int v; //value,or portfolio;
};
//二維的方式
void solveKPd(vector<KNAP> cknap, vector<vector<int> > &dp, int n, int Tw)
{
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j <=Tw; j++)
		{
			if (j<cknap[i].w) dp[i+1][j] = dp[i][j]; 
//j是每一個子問題的w上界,這裡與整體的Tw無關
			else if (dp[i][j] < dp[i][j - cknap[i].w] + cknap[i].v)
				dp[i+1][j] = dp[i][j - cknap[i].w] + cknap[i].v;
			else
				dp[i+1][j] = dp[i][j];
		}
	}
	cout << dp[n][Tw] << endl;
}
int main()
{
	int Tw; int n;
	cin >> Tw >> n;
	vector<KNAP> cknap;
	for (int i = 0; i < n; i++)
	{
		KNAP temp;
		cin >> temp.w >> temp.v;
		cknap.push_back(temp);
	}

	vector< vector<int> > dp(n+1, vector<int>(Tw+1,0));
	solveKPd(cknap, dp, n, Tw);
	system("pause");
	return 0;
}

如果還不理解, 可以把整個dp表格輸出來 (後面我寫了一個輸出程式, 之後會附完整的程式碼)

這是一個例子, n=5, Tw=10, 表頭的w表示每件物品的重量, v表示價值.


================================================

然後是我糾結炸了的

空間優化版本

看上面的輸出資料, 我們會發現其實二維表裡有很多重複的. 這是因為, 從遞迴式的特點來看, 我們只是基於第i-1層對第i層做了更新, 而第i-1層該是什麼樣還是什麼樣.

換言之, 我們只需要知道最後一層的情況, 而不需要儲存之前的結果.

看上面的表格, 其實我們最後輸出的是最右下角的值.

我們這個時候可以得到一個遞迴式

 f[w]=max{f[w], f[w-wi]+vi}

理解起來, 是和上面講的一樣的.
但是, 在具體的實現層面上, 有一個很反直覺的點:

不同於二維dp的雙重迴圈, 空間優化版本的內層迴圈必須是逆序的.

如果這一點理解了, 整個程式的實現就非常容易了.

我們可以對比一下這兩個式子:

dp[i][w]=max{dp[i-1][w],dp[i-1][w-wi]+vi}

 f[w]=max{f[w], f[w-wi]+vi}

可以發現, 在一維遞迴式裡, 要求f[w-wi]+vi 這部分 代替 dp[i-1][w-wi]+vi這部分

我們現在又只有一維陣列. 這就要保證, 在第i次外迴圈時, 呼叫的f[w-wi]實際上是基於第i-1次迴圈得到的值.

而逆序保證了, 對於f[w], 它要呼叫的f[w-wi]一定是第i層迴圈還沒有更新過的, 換言之, f[w-wi]只有可能是第i-1層儲存的資料.

比如說, 我們上面的例子, 內層迴圈從Tw=10開始往下減, 

第一個數就是求f[10]=max{f[10], f[10-wi]+vi} 

這時我們要知道的f[10-wi]其實是二維裡的dp[i-1][10-wi]

那麼我這次迴圈才從10開始, 才第一次啊!! 

根本不會去更新f[10-wi]這個數, 那麼這裡面儲存的是什麼玩意呢?

肯定是第i-1次外迴圈過一遍儲存的結果對不~~

這就是我之前說的"代替"dp[i-1][w-wi]+vi.

假設你這個時候第一個數是求的0, 一直求到了第f[10], 那麼你這個時候再去呼叫f[10-wi].

因為這一下就變成了最後一個數, 那麼排在10前面的數肯定已經被第i層的迴圈動過了

要是還原成二維遞迴式, 就變成了dp[i][w]=max{dp[i][w],dp[i][w-wi]+vi}

這顯然是有問題的.

要是看抽象的解釋還是理解不能, 我們再輸出一下

還是上面的例子 //其中, 0-6表示原來是0,更新為6

//表頭是表示 第i個物品: 重量 價值 e.g 0:2 6


比如我們考慮要放2個物品時, 第一個數是f[10]=max{f[10],f[10-2]+3}

這時候f[10-2]肯定是從上一行來的, 也就是放1個物品時得到的.

而下面假設正序輸出時, 我們會發現放第1個物品時就出了問題.

放到f[5]時, f[5]=max{f[5],f[3]+6}, 這時候第1個物品就被放了兩次.

對應上面的抽象解釋,  f[3]已經被這一層迴圈更新過了, 呼叫的是本層迴圈的值dp[i][w-wi] 

而不是上一層迴圈的值dp[i-1][w-wi]

=============================================

於是, 我們就可以寫出一維部分的核心程式碼啦~

for (int i = 0; i < n; i++)
	{
		for (int j = Tw; j >= cknap[i].w; j--) //逆序
			if (f[j - cknap[i].w] + cknap[i].v > f[j])
				f[j] = f[j - cknap[i].w] + cknap[i].v;
	}
	cout << f[Tw] << endl;

==========================================

如果看到這裡還不明白, 就用下面的cpp多輸出幾組資料試一試

再動筆手動求一求表格中某幾個值, 相信你會懂噠

//畢竟我在智商躺平的狀態下都搞懂了不是嘛=.=

下面這個程式, 有直接輸出表格的過程, 就不用手動完成整個dp表格了.

但是, 揹包問題較大時, 格式可能會很辣眼睛...如果實在有需要可以自己調整

如果只需要結果, 就把巨集定義中的uds 值改為0

如果需要二維揹包求解, 把xy改為1

附上我用的輸入樣例:

10 5
2 6
2 3
6 5
5 4

4 6

==========================================

//author: ECSoBaby, 2018/05/05, Basic Dynamic Programming

#include<iostream>
#include<vector>
using namespace std;

#define uds 1 //如果需要輸出動態規劃的過程幫助理解, 改uds為1; 否則為0
#define xy 0 //xy=1, dp二維陣列; xy=0, dp一位陣列(空間優化)
struct KNAP
{
	int w; //weight;
	int v; //value,or portfolio;
};
//二維的方式
void solveKPd(vector<KNAP> cknap, vector<vector<int> > &dp, int n, int Tw)
{
	for (int i =0; i <n; i++)
	{
		for (int j=0; j <=Tw; j++)
		{
			if (j<cknap[i].w) dp[i+1][j] = dp[i][j]; //j是每一個子問題的w上界,這裡與整體的Tw無關
			else if (dp[i][j] < dp[i][j - cknap[i].w] + cknap[i].v)
				dp[i+1][j] = dp[i][j - cknap[i].w] + cknap[i].v;
			else
				dp[i+1][j] = dp[i][j];
		}
	}
	//cout << dp[n][Tw] << endl;
#if uds
	cout << endl;
	cout << "n: w v\t\t";
	for (int j = 0; j <= Tw; j++)
		cout << j << "\t";
	cout << endl;
	cout << endl;
	for (int i = 0; i <=n; i++)
	{
		if(i==0)  cout << i << ": " <<0<<" "<<0<< "\t\t";
		else
		cout << i << ": "<<cknap[i-1].w<<" "<<cknap[i-1].v<<"\t\t";
		for (int j = 0; j <= Tw; j++)
			cout << dp[i][j] << "\t";
		cout << endl;
	}
	cout << endl;
#else
	cout << dp[n][Tw] << endl;
#endif
	
}

//一維的方式 solveKP refine
void solveKPd_re(vector<KNAP> cknap, vector<int> &f, int n, int Tw)
{
#if uds
	cout <<"\t  ";
	for (int j = Tw; j >= 0; j--)
		cout << j << "\t  ";
	cout << endl << endl;
	for (int i = 0; i < n; i++)
	{
		cout << i <<": "<<cknap[i].w<<" "<<cknap[i].v<< "\t  ";
		for (int j = Tw; j >= cknap[i].w; j--)
		{
			cout << f[j] ;
			if (f[j - cknap[i].w] + cknap[i].v > f[j])
			{
				f[j] = f[j - cknap[i].w] + cknap[i].v;
				cout << "-"<<f[j]<<"\t"; //a-b表示從a變為b
			}
			else cout << "\t";
		}
		cout << endl;
	}
	cout << "\n最終的陣列" << endl;
	cout << "\t  ";
	for (int j = Tw; j >= 0; j--)
		cout << f[j] << "\t  ";
	cout << endl << endl;

	cout << "假設是正序輸出:" << endl;
	for (int i = 0; i <= Tw; i++) f[i] = 0;//初始化
	cout << "\t  ";
	for (int j =0; j <=Tw; j++)
		cout << j << "\t  ";
	cout << endl << endl;
	for (int i = 0; i < n; i++)
	{
		cout << i << ": " << cknap[i].w << " " << cknap[i].v << "\t  ";
		for (int j = 0; j<=Tw; j++)
		{
			cout << f[j];
			if (j > cknap[i].w)
			{
				if (f[j - cknap[i].w] + cknap[i].v > f[j])
				{
					f[j] = f[j - cknap[i].w] + cknap[i].v;
					cout << "-" << f[j] << "\t"; //a-b表示從a變為b
				}
				else cout << "\t";

			}
			else cout << "\t";
		}
		cout << endl;
	}
#else

	for (int i = 0; i < n; i++)
	{
		for (int j = Tw; j >= cknap[i].w; j--) //逆序
			if (f[j - cknap[i].w] + cknap[i].v > f[j])
				f[j] = f[j - cknap[i].w] + cknap[i].v;
	}
	cout << f[Tw] << endl;
#endif
}
int main()
{
	int Tw; int n;
	cin >> Tw >> n;
	vector<KNAP> cknap;
	for (int i = 0; i <n; i++)
	{
		KNAP temp;
		cin >> temp.w >> temp.v;
		cknap.push_back(temp);
	}

#if xy
	vector< vector<int> > dp(n + 1, vector<int>(Tw + 1, 0));
	solveKPd(cknap, dp, n, Tw);
#else 
	vector<int> f(Tw + 1, 0);
	solveKPd_re(cknap, f, n, Tw);
#endif
	system("pause");
	return 0;
}

相關推薦

利用動態規劃演算法解01揹包問題->二陣列傳參->cpp記憶體管理->堆和棧的區別->常見的記憶體錯誤及其對策->指標和陣列的區別->32位系統是4G

1、利用動態規劃演算法解01揹包問題 https://www.cnblogs.com/Christal-R/p/Dynamic_programming.html 兩層for迴圈,依次考察當前石塊是否能放入揹包。如果能,則考察放入該石塊是否會得到當前揹包尺寸的最優解。 // 01 knap

動態規劃系列() 01揹包問題陣列優化

程式碼是前幾周就寫好的, 但是腦子抽了, 導致我再看時不知道是為什麼, 於是乎在CDSN上整理一下..我是愛C++和演算法的喵線童鞋 //才沒有給自己洗腦 ⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄=============================================

動態規劃系列之六01揹包問題

![](https://img2020.cnblogs.com/blog/1060878/202101/1060878-20210127165659564-1270092155.jpg) 揹包問題是動態規劃最具有代表性的問題。問題是這樣的: # 問題 法外狂徒張三是一個探險家,有一次巧合之下進入到一個有寶

演算法模板() 01揹包,多重揹包,完全揹包

01揹包 #include<bits/stdc++.h> using namespace std; int dp[300][3000]; int w[3000],v[3000]; int N,V; int main(){ cin>>N>>V; for(re

動態規劃-矩陣連乘問題(

動態規劃的理論性和實踐性都比較強,一方面需要理解狀態、狀態轉移、最優子結構、重疊子問題等概念,另一方面又需要根據題目的條件靈活設計演算法。 動態規劃是一種用途很廣的問題求解方法。它本身並不是一個特定的演算法,而是一種思想,一種手段。 動態規劃演算法與分治法類似,其基本思想也是將待求解問題

【LeetCode題解】動態規劃:從新手到專家(

【LeetCode題解】動態規劃:從新手到專家(一) 文章標題借用了Hawstein的譯文《動態規劃:從新手到專家》。 1. 概述 動態規劃( Dynamic Programming, DP)是最優化問題的一種解決方法,本質上狀態空間的狀態轉移。所謂狀態轉移是指每個階段的最優狀態(對應於

動態規劃從入門到精通()-入門篇

大三的春招,由於自己的不足,過得十分艱難。在各大公司的筆試題中,動態規劃是一個必考點。突然冒出一個想法,寫一個“動態規劃從入門到精通”系列,與各大網友一起交流學習。 學習動態規劃,愚認為,就是解決以下的三個問題: 什麼是動態規劃?什麼時候要用動態規劃?怎麼使

動態規劃算法的理解相關題目分析

自底向上 esp 它的 解包 宋體 成了 -h temp ace 1、對動態規劃算法的理解 (1)基本思想: 動態規劃算法的基本思想與分治法類似:將待求解的問題分解成若幹個子問題,先求解子問題,然後從這些子問題的解中得到原問題的解。但是,與分治法不同的是,為了避免重復多次計

陣列的排序陣列

一維陣列的排序及二維陣列 一 . 陣列的三個簡單排序: 1.氣泡排序: public static void bubbleSort(int[] array) { for(int i=0;i<array.length-1;i++) {

P1759 通天之潛水(不詳細,勿看)(動態規劃遞推,組合揹包,洛谷)

題目連結:點選進入 題目分析: 簡單的組合揹包模板題,但是遞推的同時要重新整理這種情況使用了哪些物品 ac程式碼: #include<bits/stdc++.h> using namespace std; int weigh[101],zhu[101],t[101]; stru

演算法學習(揹包佇列和棧(優化

以棧為例,之前是增加一個元素就要重新new一個比原來大1的陣列出來替換原始陣列。 public void Push(T item) { int size = Size(); T[] newarray = new T[size + 1]; for (int i =

動態規劃之0-1揹包問題(POJ3624)

有N件物品和一個容積為M的揹包。第i件物品的體積w[i],價值是d[i]。求解將哪些物品裝入揹包可使價值總和最大。每種物品只有一件,可以選擇放或者不放。(N<=3500,M<=130000)。 解題思路: 用F[i][j]表示取前i種物品,使它們總體積不超過j的最優取法取

動態規劃實現0-1揹包問題

//動態規劃實現0-1揹包 public class DN01 { public static void dy(int []v,int []w,int c,int [][]m){ int n=v.length-1; //i=n int jMax=Math.min(w[n]-1,

【演算法】動態規劃解決0-1揹包的兩個疑惑

1. 揹包問題 描述: 給定 n 種物品,每種物品有對應的重量weight和價值value,一個容量為 maxWeight 的揹包,問:應該如何選擇裝入揹包的物品,使得裝入揹包中的物品的總價值最大? 過程:   a) 把揹包問題抽象化(X1,X2,…,Xn,其中 Xi

陣列其二陣列的表示

陣列:儲存相同資料型別的集合 一維陣列:是指由相同資料型別的資料 經過 在同一方向 有序排列,一維陣列 結構單一,但卻是二維陣列、多維陣列的集合 二維陣列:一維陣列的疊加,將一維陣列看做是一個 “資料”,則將多個數據 依照一位陣列 在 另一方向上有序排列。 n維陣列:(

動態規劃之0-1揹包問題

問題: 物品集合s={1,2,3,4,…,n},物品i的重量為wi,其價值為vi,揹包的容量(最大載重量)為W,如何裝使物品價值最大。(物品不能分割) 分析: p(i,j)是揹包容量為j,可選物品為i(i+1,…,n)時的最優解 (“將前i個物品放入容量為j的揹

動態規劃之0-1揹包問題,鋼條切割

動態規劃 首先說說動態規劃:動態規劃與分治法相似,都是組合子問題的解來解決原問題的解,與分治法的不同在於:分治法的子問題是相互獨立存在的,而動態規劃應用於子問題重疊的情況。 設計動態規劃演算法的步驟: 1、刻畫一個最優解的結構特徵 2、遞迴地定義最優解的

動態規劃解0-1揹包問題(C語言版)

這學期開的演算法課,感覺好難,光這個問題就弄了好久,我這裡的程式碼非本人原創程式碼,都是借鑑網上的程式碼按自己的理解加以改進的,原網頁地址 為http://www.cnblogs.com/qinyg/archive/2012/04/26/2471829.html 問題描

分治與動態規劃(3種揹包問題)

動態規劃、分治法和貪心法都是利用求解子問題,而後利用子問題求解更上層問題,最終獲得全域性解決方案的方法。但是三者的應用場景和性質卻存在著極大的不同: 1. 分治法 分治法的精髓: 分–將問題分解為規模更小的子問題; 治–將這些規模更小的子問題逐個擊破

動態規劃解0-1揹包問題

題目如下: 試用動態規劃的方法,求解0-1揹包問題:有一揹包,能裝入物體總重量為C,有n個物體,重量為w1,w2,..,wn,價值分別為v1,v2,…vn。試求一種裝載方案,使得揹包裝載的物體總價值最大。其中,C, w都是整數。 解題思路: 簡單dp 試用動態規劃