昨天同事遇到一個優惠券使用的問題,用下班時間和早上研究了下,和動態規劃的揹包問題有關,但又不一樣於揹包,感受比較有意思就在這裏作個記錄,在羣裏討論和梳理成文字也使本身更清晰的瞭解本身知道什麼。php
問題的精簡描述爲:購買商品時,有多張滿減優惠券可用(可疊加使用),求最優策略(減免最多)。 準確描述爲:python
設共有n張優惠券C: [(V1, D1), (V2, D2), (V3, D3), ..., (Vn, Dn)],其中Vn爲面值,Dn爲減免值(對於一張優惠券Cx,滿Vx減Dx),優惠券爲單張,可疊加使用(使用過一張後,若是知足面值還可使用其餘優惠券)。求商品價值爲M時,使用優惠券的最優策略:1.減免值最多,2.優惠券剩餘最優(好比對於 C1 (2, 0.1) 、C2 (1, 0.1) 只能選擇一張的最優取捨就是用C1留C2 )。算法
輸入:數組
C = [(2, 1.9), (1, 1), (1, 0.1), (2, 0.1)] , M = 3併發
指望輸出:app
使用優惠券:[(2, 0.1), (2,1.9), (1,1)]學習
總減免:3優化
看到其餘人推薦揹包,因爲沒用過揹包算法,經過 動態算法規劃算法揹包問題 學習了下揹包的思想。順便了解一下動態規劃能解決什麼問題:spa
適用動態規劃方法求解的最優化問題應該具有的兩個要素:最優子結構和子問題重疊。——《算法導論》動態規劃原理.net
優惠券問題看起來和揹包問題很像,可是有一點不一樣
圖1 揹包問題和優惠券問題的不一樣
圖中,揹包問題裏面的數據爲:在負重已知的前提下能裝物品的最優總價值;優惠券問題裏面的數據爲總金額能使用優惠券的最優總減免值。
對於揹包問題,若是負重爲4,策略只能是拿2號物品,由於拿取2號以後負重還剩(4-3=1),再拿不了1號物品了(最終價值爲1.5);對於優惠券問題,若是金額爲4,使用完2號優惠券以後,金額還剩(4-1.5=2.5),還能夠再用1號優惠券的(最終減免值爲2.5)。
總結這個不一樣就是:揹包判斷大於重量W,再減去W,獲得剩餘值再去上一層找最優解(統計價值);優惠券則是須要判斷大於面額V,再減去減免值D,剩餘值再去上一層找最優解(統計減免值D)。
並且由於這個不一樣,優惠券問題的數據對優惠券順序是有要求的,不像揹包問題中,老是負重減物品重量,剩餘的重量直接去找上次最優再計算就行了。順序問題分兩種:
1、對於優惠券,不一樣面額的順序
圖2 優惠券面額順序對結果的影響
圖中,將物品和券的順序顛倒,對於揹包問題,最後一行數據徹底相同,對結果無影響;對於優惠券問題,順序變告終果會不同。(由於須要知足優惠券(v,d), 中的v才能減去第二項,因此對順序有要求)。因此,不一樣面額 (V不一樣) 的優惠券,應該升序排列。
2、面額相同,減免值不一樣
圖3 優惠券面額相同,不一樣減免值的順序對結果的影響
由於揹包思想是經過上一次的結果來鋪墊下一次的值,因此從上往下須要先生成同額度的最優值。因此,同面額不一樣減免值 (V同D不一樣) 的優惠券,應該降序排列。
排序示例爲:
[ (2, 1.9), (1, 1), (1, 0.1), (2, 0.1) ]
需排列爲
[ (1, 1), (1, 0.1), (2, 1.9), (2, 0.1), ]
綜以上 一點不一樣兩種順序 的狀況所述,使用揹包以前須要排序(V升D降),按V升序,若是V相同,再按D降序排。再使用揹包算法(大於V減去D)。
原本想說一句,思路有了,程序都不重要。可是,在寫的過程當中,這個排序思路(V升D降),是試出來的,而不是先想好的。因此動手仍是很重要的,否則個人腦子還想不長遠。
用的多維數組,能夠優化的點有:用一維數組存儲;間隔優化(若是優惠券有分,span爲100,那數組就很大了)。Python 版程序:
# coding:utf-8 # 揹包算法,解決滿減優惠券疊加使用問題 def coupon_bags(coupon, amount): """ 優惠券揹包算法 param: coupon 優惠券數組 param: amount 金額 """ # 轉換金額跨度(間隔): 元->角 span = 10 amount = int(amount*span) for i, v in enumerate(coupon): for j in range(len(v)): coupon[i][j] = int(coupon[i][j]*span) # 初始化結果數組,dps 存儲滿減值(揹包算法結果) ,dps_coupons 存儲優惠券 dps = [] dps_coupons = [] for i in range(len(coupon)+1): dps.append(list((0,)*(amount+1))) # list 直接 * 生成的是同一list,用循環生成 dps_coupons.append([]) for j in range(amount+1): dps_coupons[i].append([]) for i in range(1, len(coupon)+1): for j in range(1, amount+1): if j < coupon[i-1][0]: # 獲取上個策略值 dps[i][j] = dps[i-1][j] dps_coupons[i][j] = dps_coupons[i-1][j] else: if(dps[i-1][j] > dps[i-1][j-coupon[i-1][1]]+coupon[i-1][1]): # 上一行同列數據 優於 當前優惠券+剩餘的金額對應的上次數據,取以前數據 dps[i][j] = dps[i-1][j] dps_coupons[i][j] = dps_coupons[i-1][j] else: # 選取當前+剩餘 優於 上一行數據 dps[i][j] = dps[i-1][j-coupon[i-1][1]]+coupon[i-1][1] dps_coupons[i][j] = dps_coupons[i-1][j-coupon[i-1][1]].copy() dps_coupons[i][j].insert(0, tuple(coupon[i-1])) # print(f"{i} {j}, {tuple(coupon[i-1])} dps {i-1} {j-coupon[i-1][1]}:{dps_coupons[i-1][j-coupon[i-1][1]]} ") print('----------------------------------------------------') # 結果需返回數據原單位(元) result_coupons = dps_coupons[-1][-1].copy() for i, v in enumerate(result_coupons): result_coupons[i] = list(result_coupons[i]) for j in range(len(v)): result_coupons[i][j] = result_coupons[i][j]/span print(f"使用優惠券:{result_coupons} 總減免:{dps[-1][-1]/span}") # 優惠券 coupon_items = [ [1, 1], [1, 0.1], [2, 1.9], [2, 0.1], ] # 舉例中的優惠券是最終順序。確保優惠券已經排序過,多維升序(V升D降),此處省略 # sorted_coupon(coupon) coupon_bags(coupon_items, 3) """ coupon_items = [ [1, 0.6], [2, 0.7], [2, 1.3], [3, 2.3], ] coupon_bags(coupon_items, 5) """
輸出:使用優惠券:[[2.0, 0.1], [2.0, 1.9], [1.0, 1.0]] 總減免:3.0
還寫了PHP版本的,一併發上來吧。
<?php /** * 揹包算法,解決優惠券問題 * @param array $coupon 優惠券數組 * @param float $amount 金額 */ function coupon_bags($coupon, $amount) { # 轉換金額單位(跨度):角 $span = 10; $amount = intval($amount * $span); foreach ($coupon as $i => $v) { for ($j = 0; $j < count($v); $j++) { $coupon[$i][$j] = intval($coupon[$i][$j] * $span); } } # 結果,多數組 $dps = []; $dps_coupons = []; for ($i = 0; $i <= count($coupon); $i++) { for ($j = 0; $j <= $amount; $j++) { $dps[$i][$j] = 0; $dps_coupons[$i][$j] = []; } } # 排序,多維升序(內降) # sort_coupon($coupon); for ($i = 1; $i <= count($coupon); $i++) { for ($j = 1; $j <= $amount; $j++) { if ($j < $coupon[$i - 1][0]) { # 獲取上個策略值 $dps[$i][$j] = $dps[$i - 1][$j]; $dps_coupons[$i][$j] = $dps_coupons[$i - 1][$j]; } else { if ($dps[$i - 1][$j] > $dps[$i - 1][$j - $coupon[$i - 1][1]] + $coupon[$i - 1][1]) { # 上一行同列數據 優於 當前優惠券+剩餘的金額對應的上次數據,取以前數據 $dps[$i][$j] = $dps[$i - 1][$j]; $dps_coupons[$i][$j] = $dps_coupons[$i - 1][$j]; } else { # 選取當前+剩餘 優於 上一行數據 $dps[$i][$j] = $dps[$i - 1][$j - $coupon[$i - 1][1]] + $coupon[$i - 1][1]; $dps_coupons[$i][$j] = $dps_coupons[$i - 1][$j - $coupon[$i - 1][1]]; $dps_coupons[$i][$j][] = $coupon[$i - 1]; } } } } # 結果需返回數據原單位(元) $t = end($dps_coupons); $t2 = end($dps); $result_coupons = array_reverse(end($t)); $result_dps = end($t2); foreach($result_coupons as &$v){ foreach($v as &$v2){ $v2 = $v2/$span; } } $result_dps/=$span; echo "\n使用優惠券:". print_r($result_coupons, true). "總減免:{$result_dps}."; } $coupon_items = [ [1, 1], [1, 0.1], [2, 1.9], [2, 0.1], ]; coupon_bags($coupon_items, 3);
算法思想很重要。多思考多動手多交流。若是發現了漏洞,請您不吝賜教。