昨天同事遇到一個優惠券使用的問題,用下班時間和早上研究了下,和動態規划的背包問題有關,但又不同於背包,感覺比較有意思就在這里做個記錄,在群里討論和梳理成文字也使自己更清晰的了解自己知道什么。
問題描述
問題的精簡描述為:購買商品時,有多張滿減優惠券可用(可疊加使用),求最優策略(減免最多)。 准確描述為:
設共有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
期望輸出:
使用優惠券:[(2, 0.1), (2,1.9), (1,1)]
總減免:3
看到其他人推薦背包,由於沒用過背包算法,通過 動態算法規划算法背包問題 學習了下背包的思想。順便了解一下動態規划能解決什么問題:
適用動態規划方法求解的最優化問題應該具備的兩個要素:最優子結構和子問題重疊。——《算法導論》動態規划原理
優惠券問題看起來和背包問題很像,但是有一點不同
一點不同
圖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)。
而且因為這個不同,優惠券問題的數據對優惠券順序是有要求的,不像背包問題中,總是負重減物品重量,剩余的重量直接去找上次最優再計算就好了。順序問題分兩種:
兩種順序
一、對於優惠券,不同面額的順序
圖2 優惠券面額順序對結果的影響
圖中,將物品和券的順序顛倒,對於背包問題,最后一行數據完全相同,對結果無影響;對於優惠券問題,順序變了結果會不一樣。(因為需要滿足優惠券(v,d), 中的v才能減去第二項,所以對順序有要求)。所以,不同面額 (V不同) 的優惠券,應該升序排列。
二、面額相同,減免值不同
圖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);
Java 版的代碼:(粗糙)
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
class CouponTicket{
List<int[]> cp = new LinkedList<int[]>();
public CouponTicket copy(){
CouponTicket r = new CouponTicket();
for(int[] c : cp){
r.cp.add(c);
}
return r;
}
public String toString(){
StringBuilder s = new StringBuilder();
for(int[] c: cp){
s.append(Arrays.toString(c)).append(" - ");
}
return s.toString();
}
}
/**
* 背包算法,解決滿減優惠券疊加使用問題
*
*/
public class Coupon{
public static void main(String[] args){
double [][]coupon_items = {
{1, 1},
{1, 0.1},
{2, 1.9},
{2, 0.1},
};
Coupon.couponBags(coupon_items, 3);
}
public static void couponBags(double[][] coupon, double amount){
// 轉換成int的金額精度
int span = 10;
int amountInt = (int)(amount*span);
int couponInt[][] = new int[coupon.length][2];
// 初始化結果數組,dps 存儲滿減值(背包算法結果) ,dps_coupons 存儲優惠券
int[][] dps = new int[coupon.length+1][amountInt+1];
CouponTicket[][] dps_coupons = new CouponTicket[coupon.length+1][amountInt+1];
for(int i=0; i<coupon.length; i++){
for(int j=0; j<coupon[i].length; j++){
couponInt[i][j] = (int)(coupon[i][j]*span);
}
}
// 計算
for(int i=1; i<=coupon.length; i++){
for(int j=1; j<=amountInt; j++){
// System.out.printf("%d %d coupon[%d][0]=%s %b " ,i,j,i-1,couponInt[i-1][0], (j<couponInt[i-1][0]));
if(j < couponInt[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-couponInt[i-1][1]]+couponInt[i-1][1]){
// 上一行同列數據 優於 當前優惠券+剩余的金額對應的上次數據,取之前數據
dps[i][j] = dps[i-1][j];
dps_coupons[i][j] = dps_coupons[i-1][j];
}
else{
if(dps_coupons[i][j] == null){
dps_coupons[i][j] = new CouponTicket();
}
// 選取當前+剩余 優於 上一行數據
dps[i][j] = dps[i-1][j-couponInt[i-1][1]]+couponInt[i-1][1];
if(dps_coupons[i-1][j-couponInt[i-1][1]] != null){
dps_coupons[i][j] = dps_coupons[i-1][j-couponInt[i-1][1]].copy();
}
dps_coupons[i][j].cp.add(couponInt[i-1]);
// System.out.printf("%s dps %d %s", Arrays.toString(couponInt[i-1]), j-couponInt[i-1][1],dps_coupons[i-1][j-couponInt[i-1][1]]);
}
}
// System.out.println();
}
}
System.out.println("優惠券使用和總滿減金額如下:(優惠券未轉換原金額)");
System.out.println(dps_coupons[coupon.length][amountInt]);
System.out.println(dps[coupon.length][amountInt]/(double)span);
}
}
總結
算法思想很重要。多思考多動手多交流。如果發現了漏洞,請您不吝賜教。