c++ 多重背包狀態轉移方程_動態規劃入門——詳解經典問題零一背包

c90d52cf1bba1620b4f3843e1bc1e444.png

本文始發于個人公眾號:TechFlow,原創不易,求個關注

今天是周三算法與數據結構專題的第12篇文章,動態規劃之零一背包問題。

在之前的文章當中,我們一起探討了二分、貪心、排序和搜索算法,今天我們來看另一個非常經典的算法——動態規劃

在acm-icpc競賽領域,動態規劃是一個非常大的范疇,當中包含了許多變種,而且很多變種難度極大。比如在各種樹上和圖上以及其他數據結構上做動態規劃,這會使得問題非常復雜。好在非競賽選手并不需要了解到那么深入,一般來說,吃透背包九講,就足夠笑傲各種面試了。所以周三的算法專題我們開始全新的篇章——背包系列,今天和大家分享背包九講中的第一講,也是最簡單的零一背包問題。

背包和零一背包

沒有競賽經驗的同學在看到這個標題的時候可能會一頭霧水,動態規劃和背包有什么關系。其實沒有關系,我也不是陳奕迅的粉絲,只是當初最經典的動態規劃問題用背包做了題面,還引發出了各種變種。后來在教學的時候為了方便,于是沿用了前人的名稱。

之前我們在怪盜基德偷寶石的問題當中提到過背包問題,其實很簡單,就是說我們當下有一個容量是V的背包,和n個體積分別是v[i],價值是w[i]的五品。請問,在背包容量允許的前提下,我們最多能夠獲得多少價值的物品

由于每種物品只有一個,也就是物品只有拿和不拿兩種狀態,所以這個問題被稱為零一背包問題

貪心與反例

這種問題我們最先想到的就是貪心法,比如優先拿價值大的物品,或者是性價比高的物品,但是我們很容易構思出反例。

舉個例子,比如背包的容量是10,我們有3個物品,體積分別是6,5,5,價值是10,8,8。這個反例可以證明兩種貪心策略都不生效,因為價值最大的是10,它的體積是6,我們一旦拿了它就沒有空間再繼續獲取其他物品,而顯然拿兩個5的情況是最優的。同樣,體積是6的物品也是性價比最高的,性價比優先的貪心策略同樣不生效。

實際上不僅這兩種貪心策略不生效,所有能夠想到的貪心策略都不生效。這個問題看起來簡單,但是并不是那么容易解決。實際上這個問題一直困擾著計算學家,直到上世紀六十年代,動態規劃算法橫空出世,完美地解決了這個問題。

動態規劃

動態規劃算法的英文是dynamic programming,算是很直白的翻譯了。規劃我們都很好理解,但是動態應該怎么理解呢?又怎么來動態地規劃呢?關于這個問題的思考直接關系到算法的本質。

動態規劃算法的本質是狀態的記錄和轉移,我們結合剛才的問題,有沒有想過為什么貪心算法不可行?其實很簡單,因為我們沒辦法確定背包什么狀態是完美的。雖然我們知道背包的容量是V,但是我們并不知道最優的情況下我們能裝多少,最優的結束狀態是什么。我們把空間V看成了一個狀態來進行貪心,貪心得到的結果是最優的,但是只是貪心能達到的狀態的最優解,并不是全局的最優解,因為背包容量的限制,很有可能我們貪心策略下無法達到真正最優的狀態。

用剛才的例子解釋一下上面這段話,在貪心算法下,我們會選取容量是6,價值是10的物品,這個物品拿取了之后背包的狀態是6,獲取的價值是10。這個狀態是貪心能夠達到的最終狀態,對于這個狀態而言,它是最優解,但是這個狀態并不是整體最優的情況,因為在貪心策略下,無法達到容量10全用完的狀態。

理解了這個問題之后,再去推導解法就順其自然了。貪心策略可以獲取一些狀態最優的情況,那么我們能不能記錄下所有狀態能夠達到的最優的情況,最后在這些最優的情況當中選取一個最優的,它不就是整體最優解了嗎?

動態規劃正是基于上述思路展開的,它解決的不是一個狀態的最優解,而是所有狀態的最優解。

狀態與轉移

看到這里,你肯定還沒理解動態規劃算法,但是應該已經有一些大概的感覺了。這是對的,有正確的感覺是正確認識的前提。我們循序漸進,再來看狀態這個概念。

我們剛才提了這么多次,究竟狀態是什么呢?這是一個比較抽象的概念,在不同的問題當中它有著不同的含義。在背包問題當中,狀態指就的是背包容量的使用情況。由于背包問題中物品的體積是整數,顯然背包容量的可能性是有限的,這點不起眼,但是很重要,如果狀態不是整數,那么雖然存在動態規劃的可能,但是代碼實現可能比較麻煩。

明白了背包的容量是狀態之后,我們可以進一步想明白,背包的容量是會變化的。變化的原因是因為我們往里面放了東西,放了東西之后,背包的狀態會發生變化,會從一個狀態轉移到另一個狀態。狀態的轉移中間伴隨著我們放入東西,我們放的物品并不是固定的,而是有多種選擇的,我們決定放入A而不是BC,這是一種決策,決策會帶來狀態的轉移,不同的決策會帶來不同的轉移。

比如當前有一個背包,它的容量是10,我們在其中已經放入了一個體積是3,價值是7的物品。如果這個時候,我們經過選擇再放入一個體積是4,價值是5的物品。那么顯然,背包占用的容量會變成7,價值會變成12。這個過程就是一個經典的狀態轉移過程,這也是整個動態規劃算法的核心。

基本的概念和思想已經介紹完了,接下來就是用這些概念來解決實際問題了。

最優狀態

我們前文說了,動態規劃最后會獲取所有狀態的最優解,再從中選取全局最優的。那么它是怎么獲取局部最優解的呢?

在回答這個問題之前,我們先來思考兩個問題。

首先,假如我們已經知道了背包體積是3時的最大價值是5,這時候我們決定放入一個體積是4,價值是5的物品,那么背包的體積會增加到7,那么這個時候獲得的是體積6的最優解嗎?

這個問題不難回答,我們稍微想想就知道,很有可能不是。舉個最簡單的例子,假如我們有一個體積是7,價值是20的物品。那么顯然要比放這兩個物品更優。雖然狀態3最多能獲得價值5,狀態7也可以由狀態3轉移得到,但是這并不一定是最優的。也就是說最優的狀態轉移出去,并不一定也能得到其他狀態的最優值

我們把問題反過來就不一樣了,如果我們知道了體積6的最優解,并且還知道它是由體積等于4轉移得到的,那么我們能不能確定體積4的狀態也是對應的最優解?

這次的答案就變了,是正確的,因為如果體積4時還有更好的解法,那么體積6理應也會變得更好才對,這和我們的假設矛盾了。

我們總結一下上面的兩個結論,也就是說局部最優的情況轉移出去并不一定是最優的,但是局部最優一定也是由其他局部最優的狀態轉移得到的。這句話有點像繞口令,但是我覺得應該不難解釋。就好比學霸去考試,不一定能考第一,但是考到第一的一定是學霸。局部最優就是學霸,轉移就是考試。局部最優轉移出去并不一定是轉移之后狀態的最優,有可能還有其他更好的轉移策略,但是對于某個狀態最優的情況而言,它一定也是從之前的某個最優狀態轉移得到的。

并且狀態的轉移也是有順序的,比如在這題當中,背包當中放入了物品體積只可能增加,不可能減小,意味著狀態只能從小的轉移到大的。

我們捋一捋思路,已經很明確了,狀態可以轉移,狀態的轉移有順序,局部最優一定是由其他局部最優轉移得到的。由于我們并不知道當前的轉移能否達到最優狀態,所以我們需要用一個數組或者是容器來記錄所有狀態歷史上曾經達到過的最值。最后從所有的最值當中再選出一個最值來,就是最后問題的解。

到這里,如果是一般的動態規劃問題,已經解決了,但是零一背包還有一個細節需要考慮。

無后效性

我們先來看下整個的計算流程,首先我們需要從最初狀態開始,這個最初的狀態很好辦,就是背包是空的時候,這時候的價值是0,體積也是0,這也是它的最優狀態,這個很好理解,因為我們不能無中生有。

所以我們從0開始轉移狀態,狀態轉移伴隨著決策,在這題當中體現在選取不同的物品上。我們遍歷物品,作為決策,再遍歷能夠應用這些決策的狀態,就拿到了所有的狀態轉移。最后,我們用一個容器記錄一下所有狀態轉移過程當中達到的局部最優解,于是就結束了。

這個過程看起來非常正常,沒有任何異常,但實際上,問題來了。

我們還用剛才的題面舉例,背包容量是10,3個物品,體積分別是6,5,5,價值是10,8,9。我們從0開始拿取第三個物品,轉移到了狀態5,此時的價值是9。這個時候,我們繼續往后遍歷的話還會遍歷到狀態5,它已經是拿取了物品3,價值9的信息了。因為一個物品只能拿一次,所以我們不能再用物品3轉移狀態5,否則就違反了題意

你可能會說這個問題不難,我們可以在狀態當中也記錄之前做過的決策嘛,只要在決策的時候加一個判斷就好了。

表上面看是因為物品不能重復拿的限制,實際上是因為我們的狀態之間會有影響。也就是說我們前面做的決策很有可能影響后面的狀態做決策,這種狀態之間的前后影響稱為后效性。顯然在有后效性的場景下我們是不能使用動態規劃算法的,并不是所有問題都可以通過加上判斷解決,我們需要解決后效性這個本質問題,也就是說我們要想辦法消除后效性。

在這個問題當中,這一點很容易做到。我們只需要控制一下狀態和決策的遍歷順序,將之前的決策與之后的決策分開,使它們互不影響即可。如果我們先遍歷狀態,再遍歷每個狀態可以采取的措施,這樣必然會造成前后影響。因為前面做了的決定,后面就不能再做。但是后面并不能感知前面究竟做了什么決定。所以比較好的方法是先遍歷決策,再來遍歷可以采取這個決策的狀態。為了避免決策前后的互相影響,我們采取倒序的方式遍歷狀態

我們舉個例子,假設背包容量還是10,我們枚舉的第一個物品體積是3,價值是5。我們倒敘遍歷狀態7到0,因為對于大于7的狀態而言,并不能采取這個決策(總體積會超)。因為對于大于7的狀態而言,我們不能采取這個決策(總體積會超過限制),對于狀態7而言,我們可以采取這個決策,轉移到狀態10。我們并不知道這樣轉移會不會達成最優,所以我們這樣來記錄:dp[10] = max(dp[10], dp[3] + 5).

我們接著遍歷體積6,可以轉移到狀態9。

由于我們是倒序遍歷,所以當我們用狀態7更新狀態10時,狀態7本身并沒有被這個決策更新過。即使后面我們在遍歷到狀態4時更新了狀態7,也不會影響狀態10的結果。因為是倒序遍歷的,我們不會再用同一個策略更新到狀態10了。如果是正序遍歷,則無法避免。同樣的物品,我們很有可能會出現,用狀態1更新狀態4,再用狀態4更新狀態7,再用狀態7更新狀態10的情況出現。而這種情況其實對應了使用了多個同樣的物品,這就和題意矛盾了。

舉個例子,假設有一個物品體積是2,它的價值是5。我們遍歷狀態0的時候,會更新狀態2,我們遍歷到了狀態2,又用同樣的物品更新了狀態4,得到了10。那么對于狀態4而言,它其實相當于拿了2個這個物品,也就是說被同一個決策更新了兩次。但是我們的物品最多只有一個,顯然就不對了。

5b7889754b85df58b901fd46460f46e7.png

動態規劃當中因為無法判斷當前枚舉的狀態的來源,所以不允許出現后效性,如果解決不了則不能使用動態規劃。這也是動態規劃最基本的原則,在這題當中,我們是巧妙變換了決策和狀態枚舉的過程,消除了后效性。在其他題目當中未必相同,我們需要根據實際情況進行判斷。

如果你在做題思考的過程當中忘記了動態規劃的前提,就想想零一背包當中拿取物品的情況。物品只有一個,只能拿一次。前面拿過了后面還能拿,就違反了后效性。

狀態轉移方程

我們整理一下剛才關于狀態轉移的思路,有以下幾點:

  1. 我們從狀態0開始,狀態0的最優價值是0.
  2. 考慮后效性的問題,確保沒有后效性
  3. 執行決策的時候,會發生狀態轉移,記錄狀態對應的最優解

在這個問題當中,決策就是獲取物品,狀態就是背包容量。由于拿取物品會引起背包容量變化,并且每個物品只有一個,為了避免產生后效性,我們需要先枚舉決策,再枚舉狀態,保證每個決策只在每個狀態上最多應用一次。在此過程當中,需要一直記錄每個狀態的最優解。

由于背包的容量是V,我們只需要用一個容量是V的數組就足夠記錄所有的狀態。

dp

dp記錄的是所有的狀態,我們用max(dp[v+i.v], dp[v] + i.w)來更新dp[v+i.v]狀態的值,由于當前的決策不一定比之前的更好,所以要加上max操作,保證每個狀態記錄下來的結果都是它最優的。當所有的狀態的最優解都有了之后,顯然整個問題的最優解也在其中了。

上面這個記錄狀態轉移過程的式子叫做狀態轉移方程,它也是動態規劃算法的核心概念。很多時候,在我們解動態規劃問題的時候,會在草稿紙上推演狀態轉移方程。如果狀態轉移方程能清楚地列出來,距離寫出代碼也就不遠了。

代碼

上面的轉移方程已經非常接近最后的代碼了,真正寫出來也就只有幾行而已:

dp 

總結

關于零一背包的前后推導以及當中所有的概念始末就算是介紹完了,雖然我們用了這么多篇幅來介紹這個算法,但是真正寫成代碼也就只有短短幾行。單從代碼行數來看,動態規劃可以說是實現代碼最短的算法了,只是雖然它代碼不長,但是思路并不簡單,尤其是當中的下標以及循環的順序等細節,希望大家不要掉以輕心。

今天零一背包的問題到這里就結束了,下周的算法專題我們繼續背包問題,來看看01背包的進階版——完全背包和多重背包問題,敬請期待。

如果覺得有所收獲,請順手點個關注或者轉發吧,你們的舉手之勞對我來說很重要。

ac3a155a86ebb4cb962c95af1d8a5f7a.png

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/374436.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/374436.shtml
英文地址,請注明出處:http://en.pswp.cn/news/374436.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Discuz! 的編碼規范

前言 本規范由編程原則組成,融合并提煉了開發人員長時間積累下來的成熟經驗,意在幫助形成良好一致的編程風格。適用范圍 如無特殊說明,以下規則要求完全適用于Discuz!項目,同時也可大部分適用于COMSENZ旗下其他PHP項目。標準化的重…

C語言代碼規范(三)if語句

一、整型變量與0比較 許多人為了一時之便,模仿布爾變量風格寫為如下代碼 if(value) {... }if(!value) {... } 應當用 或 ! 來與0比較 if(0 value) {... }if(0 ! value) {... } 二、當if內的語句是與常量進行比較時,常量為左值,變量為右…

6月24 面向對象的設計原則-----工廠模式和單列模式

工廠模式: 工廠模式就是專門負責將大量有共同接口的類實例化,而且不必事先知道每次是要實例化哪一個類的模式。它定義一個用于創建對象的接口,由子類決定實例化哪一個類。 工廠模式相當于創建實例對象的new,經常要根據類Class生成…

LeetCode Subsets

原題鏈接在這里:https://leetcode.com/problems/subsets/ 題目: Given a set of distinct integers, nums, return all possible subsets. Note: Elements in a subset must be in non-descending order.The solution set must not contain duplicate su…

使用ThreadPoolExecutor并行化獨立的單線程任務

Java SE 5.0中引入的任務執行框架是簡化多線程應用程序的設計和開發的巨大飛躍。 該框架提供了用于管理任務概念,管理線程生命周期及其執行策略的工具。 在此博客文章中,我們將描述該框架的功能,靈活性和簡單性,以展示一個簡單的用…

python定義一個圓_Python-矩形和圓形

原博文 2019-11-11 12:34 ? Exercise 15.1. 定義一個叫做Circle 類,類的屬性是圓心 (center) 和半徑 (radius) , 其中,圓心 (center) 是一個 Point 類,而半徑 (radius) 是一個數字。 實例化一個圓心 (center) 為 (150, 100) ,半…

C語言代碼規范(四)命名規則

一、宏定義全部字母大寫,單詞間下劃線間隔 #define FLASH_PAGE_SIZE 256 #define FLASH_SECTOR_SIZE (4 * 1024) #define FLASH_BLOCK_SIZE (64 * 1024) #define FLASH_SIZE (16 * 1024 * 1024) 二、const修飾的常量全部字母大寫,單詞間…

Forbidden You don't have permission to access / on this server PHP

Forbidden You dont have permission to access / on this server PHP 在新安裝的谷歌游覽器里,打不了PHP網站了,錯誤顯示: Forbidden You dont have permission to access / on this server. 原因還是配置權限問題 解決辦法: wa…

Spring 3.1和JPA的持久層

1.概述 本教程顯示了如何使用Hibernate作為持久性提供程序使用JPA設置Spring 。 有關使用基于Java的配置和項目的基本Maven pom設置Spring上下文的分步介紹,請參閱本文 。 2. Java的JPA Spring配置 要在Spring項目中使用JPA, 需要設置EntityManager 。…

150928錯誤認識

1. $arr array(); foreach ($re as $k>$v){  $arr[] $v[updatetime];} $arr的返回結果為: Array ([0] > 2014-09[1] > 2015-04[2] > 2015-09 )$arr array(); foreach ($re as $k>$v){  $arr[$k] $v[updatetime];} $arr的返回結果為&#xff…

STM32F1筆記(一)GPIO輸出

GPIO:General Purpose Input Output (通用輸入/輸出)。 GPIO最經典應用:LED燈。 先看電路。聲明:參考正點原子戰艦開發板。 與LED串聯的電阻稱為限流電阻。 限流電阻計算公式:R(U-LED壓降)/20ma。 U為LE…

dataframe轉化為array_【Python專欄】12 種高效 Numpy 和 Pandas 函數為你加速分析

來源:機器之心編譯:Jamin、杜偉、張倩我們都知道,Numpy 是 Python 環境下的擴展程序庫,支持大量的維度數組和矩陣運算;Pandas 也是 Python 環境下的數據操作和分析軟件包,以及強大的數據分析庫。二者在日常…

具有GlassFish和一致性的高性能JPA –第1部分

您以前聽說過連貫性嗎? 大概是。 它是那些著名的內存網格解決方案之一,該解決方案承諾了超快的數據訪問速度和對經常使用的數據的無限空間。 一些眾所周知的競爭對手是Infinispan , Memcached和Terracotta Ehcache 。 它們都很棒,…

如何在自己的代碼中實現分享視頻文件或者是圖片文件到微信 QQ微博 新浪微博等!!!...

首先在文檔第一句我先自嘲下 , 我是大傻逼, 弄了兩天微信是視頻分享,一直被說為啥跟系統的相冊分享的不一樣,尼瑪!!! 這里來說正文,我這里不像多少太多,大家都是程序猿&a…

sql 數據庫中用創建好的視圖修改表數據

只要滿足下列條件,即可通過視圖修改基礎基表的數據: 1、任何修改(包括 UPDATE、INSERT 和 DELETE 語句)都只能引用一個基表的列。 2、視圖中被修改的列必須直接引用表列中的基礎數據。不能通過任何其他方式對這些列進行派生&#…

boost原理與sklearn源碼_機器學習sklearn系列之決策樹

一、 Sklearn庫 Scikit learn 也簡稱 sklearn, 自2007年發布以來,scikit-learn已經成為Python重要的機器學習庫了。支持包括分類、回歸、降維和聚類四大機器學習算法。還包含了特征提取、數據處理和模型評估三大模塊。sklearn是Scipy的擴展,建立在NumPy和…

STM32F1筆記(二)GPIO輸入

STM32 GPIO輸入的經典應用是按鍵。 先看電路。聲明:參考正點原子戰艦開發板。 在這里可以看到,KEY_UP按鍵是高電平有效的,即當按下該按鍵時,GPIO讀到高電平。 KEY0/1/2是低電平有效的,即當按下該按鍵時,G…

Google Authenticator:將其與您自己的Java身份驗證服務器配合使用

用于移動設備的Google Authenticator應用程序是一個非常方便的應用程序,它實現了TOTP算法(在RFC 6238中指定)。 使用Google Authenticator,您可以生成時間密碼,該密碼可用于在共享請求用戶密鑰的身份驗證服務器中授權用…

[Week2 作業] 代碼規范之爭

這四個問題均是出自 http://goodmath.scientopia.org/2011/07/14/stuff-everyone-should-do-part-2-coding-standards/ 。 我對這四個問題均持反駁的看法,下面是我的理由~ Q1:這些規范都是官僚制度下產生的浪費大家的編程時間、影響人們開發效率, 浪費時…

STM32F1筆記(三)UART/USART

UART:Universal Asynchronous Receiver/Transmitter(通用異步收/發器) USART:Universal Synchronous/Asynchronous Receiver/Transmitter(通用同步/異步串行收/發器) 從命名即可看出USART就是UART的基礎上…