算法模板——动态规划(未完待更)

1 动态规划初探

1.1 状态和转移方程

【核心思想】动态规划问题的一般形式就是求最值


为什么说动态规划一般形式就是求最值呢?

这里给出一个概念『最优子结构』:如果问题的最优解包含的子问题的解也是最优的,就称该问题具有最优子结构,同时满足最优化原理

要符合『最优子结构』,子问题间必须相互独立

动态规划问题一定会具备『最优子结构』,因此就通过子问题的最值得到原问题的最值


【思维框架】

明确『状态』 -> 定义 dp 数组/函数的含义 -> 明确『选择』-> 明确 base case

但是,光光这么说还是会觉得很难定义出合适的状态和转移方程,使得递归正常地进行,毕竟不同的状态定义和状态转移方程都会使得解法的复杂度不同

所以,我们接从递归看起,我个人觉得下面例子真的是很好地从递归出发讲清楚了动态规划是什么 b( ̄▽ ̄)d

1.2 从递归看动态规划

【经典问题分析——凑零钱】

题目描述322. 零钱兑换

给你 k k k 种面值的硬币, 面值分别为 c 1 , c 2 . . . c k c_1, c_2 ... c_k c1,c2...ck , 每种硬币的数量无限, 再给⼀个总金额 a m o u n t amount amount , 问你最少需要几枚硬币凑出这个金额, 如果不可能凑出, 算法返回 − 1 -1 1

1. 暴力递归

明确『状态』: 也就是原问题和子问题中变换的量,由于硬币数量无限,所以唯一的状态就是目标金额 a m o u n t amount amount

定义 dp 函数的含义: 当前的目标金额是 n n n,至少需要 d p ( n ) dp(n) dp(n) 个硬币凑出该金额

明确『选择』: 也就是对于每个状态,可以做出什么选择改变当前状态。具体到当前的问题上,无论当前的目标金额是多少,『选择』就是从面额列表 c o i n s coins coins 中选择一个硬币,然后目标金额就会减少

def coinChange(coins: List[int], amount: int):
	# 定义: 要凑出⾦额 n, ⾄少要 dp(n) 个硬币
	def dp(n):
	# 做选择, 选择需要硬币最少的那个结果
		for coin in coins:
			res = min(res, 1 + dp(n - coin))
	return res
	# 我们要求的问题是 dp(amount)
	return dp(amount)

明确 base case: 显然目标金额为 0 时,所需硬币数量为 0;当目标金额小于 0 时,无解,返回 -1

def coinChange(coins: List[int], amount:int):
	
	def dp(n):
		# base case
		if n == 0: return 0
		if n < 0: return -1
		# 求最小值,所以初始化为正无穷
		res = float('INF')
		for coin in coins:
			subproblem = dp(n - coin)
			# 子问题无解,跳过
			if subproblem == -1: continue 
			res = min(res, 1 + subproblem)
		return res if res != float('INF') else -1
	
	return dp(amount)

至此,状态转移方程其实已经完成了

我们来看一下 a m o u n t = 11 , c o i n s = { 1 , 2 , 5 } amount=11, coins=\left \{1, 2, 5\right \} amount=11,coins={1,2,5} 时的递归树
在这里插入图片描述
时间复杂度:子问题总数 × 每个子问题的时间

子问题总数为递归树中结点的个数,这个比较难看出来,是 O ( n k ) \mathcal O(n^k) O(nk);每个子问题中含有一个 for 循环,复杂度为 O ( k ) \mathcal O(k) O(k),故暴力递归下的总时间复杂度为 O ( k × n k ) \mathcal O(k\times n^k) O(k×nk)

2. 带备忘录的递归

def coinChange(coin, amount):
	# 备忘录
	memo = dict()
	def dp(n):
		# 查备忘录,避免重复计算
		if n in memo: return memo[n]
		
		if n == 0: return 0
		if n < 0: return -1
		res = float('INF')
		for coin in coins:
			subproblem = dp(n - coin)
			if subproblem == -1: continue
			res = min(res, 1 + subproblem)
		
		# 记入备忘录
		memo[n] = res if res != float('INF') else -1
		return memo[n]
	
	return dp(amount)

很显然 『备忘录』大大减小了子问题数目,完全消除了子问题冗余的问题,所以子问题总数不会超过金额数 n n n,即子问题复杂度为 O ( n ) \mathcal O(n) O(n),处理一个子问题的时间不变,仍是 O ( k ) \mathcal O(k) O(k),所以总时间复杂度为 O ( k n ) \mathcal O(kn) O(kn)

3. dp 数组的迭代解法

在上面添加备忘录的代码中已经很接近 dp 的思想了,下面我们看一下 dp 数组的完整解法

d p [ i ] = x   表 示 当 目 标 金 额 为   i   时 ,   至 少 需 要   x   枚 硬 币 dp[i] = x\ 表示当目标金额为\ i\ 时,\ 至少需要\ x\ 枚硬币 dp[i]=x  i   x 

def coinChange(coins, amount):
    dp = [amount + 1] * (amount + 1)
    dp[0] = 0
    for i in range(len(dp)):
        for coin in coins:
            if i - coin < 0: continue
            dp[i] = min(dp[i], 1 + dp[i - coin])
    return dp[amount] if dp[amount] != amount + 1 else -1

在这里插入图片描述

2 背包问题

2.1 0-1背包、满包

N N N 件物品和一个容量为 V V V 的背包。放入第 i i i 件物品耗费的空间是 C i C_i Ci,得到的价值是 W i W_i Wi,求解将哪些物品装入背包可使价值总和最大

基本思路

f ( i , j ) f(i, j) f(i,j) 表示前 i i i 件物品放入一个容量为 j j j 的背包可以获得的最大价值

于是,转移方程可以表述为

f ( i , j ) = m a x ( f ( i − 1 , j ) , f ( i − 1 , j − w i ) + v i ) f(i,j) = max(f(i-1, j), f(i-1,j-w_i)+v_i) f(i,j)=max(f(i1,j),f(i1,jwi)+vi)

//二维
for(int i=1;i<=n;i++)
{
	for(int j=1;j<=v;j++)
	{
		if(j>=volume[i])
			dp[i][j] = max(dp[i-1][j], m[i-1][j-volume[i]]+value[i]);
		else
			dp[i][j] = dp[i-1][j];
	}
}

优化空间复杂度

// 一维
for(int i=1;i<=n;i++) 
{
	for(int j=v;j>=volume[i];j--)
		dp[j] = max(dp[j], dp[j-volume[i]]+value[i]);
}

满包

满背包与普通背包唯一不同的点就是恰好装满背包所能取得的最大价值,主要区别在于初始化,满包问题的初始条件: f ( 1... v ) = − ∞ ,   f ( 0 ) = 0 f(1...v)=-\infty,\ f(0)=0 f(1...v)=, f(0)=0

可以这样理解:

f [ . . . ] f[...] f[...] 的初始化状态表示的是没有任何物品可以放入背包时的合法状态,如果要求背包恰好装满,那么初始化 f ( 1... v ) = 0 f(1...v)=0 f(1...v)=0 就不对了,因为此时表示恰好装满容量为 v v v 的背包时物品价值为 0,所以在满包问题中初始化条件为 f ( 1... v ) = − ∞ ,   f ( 0 ) = 0 f(1...v)=-\infty,\ f(0)=0 f(1...v)=, f(0)=0

2.2 完全背包

N N N 种物品和一个容量为 V V V 的背包,每种物品都有无限件可用。放入第 i i i 种物品的耗费的空间是 C i C_i Ci,得到的价值是 W i W_i Wi

求解:将哪些物品装入背包,可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。

for(int i=1;i<=n;i++) 
{
	for(int j=volume[i];j<=v;j++)
		dp[j] = max(dp[j], dp[j-volume[i]]+value[i]);
}

这个代码与 0-1 背包的代码只有 j j j 的循环次序不同而已。为什么这样一改就可行呢?

首先,想想为什么 0-1 背包中要按照 j = V . . . 0 j=V...0 j=V...0 的逆序来循环?这是因为要保证第 i i i 次循环中的状态 f [ i ] [ j ] f[i][j] f[i][j] 是由状态 f [ i − 1 ] [ j − w [ i ] ] f[i−1][j−w[i]] f[i1][jw[i]] 递推而来。换句话说,这正是为了保证每件物品只选一次,在考虑 “ 选入第 i i i 件物品 ” 时的策略,是依据一个没有选入第 i i i 件物品的子结果 f [ i − 1 ] [ j − w [ i ] ] f[i−1][j−w[i]] f[i1][jw[i]]

而现在完全背包的特点恰是每种物品可选无限件,所以在考虑 “ 加选一件第 i i i 种物品 ” 时,却正需要一个可能已选入第 i i i 种物品的子结果 f [ i ] [ j − w [ i ] ] f[i][j−w[i]] f[i][jw[i]],所以就可以并且必须采用 j = 0... V j=0...V j=0...V的顺序循环。这就是这个简单的程序为何成立的道理

2.1.3 多重背包

给n种物品和一个容量为v的背包,每种物品最多有num[i]件可用,每个物品都有一个体积volume[i]和价值value[i],求背包最多能装多少价值的物品?

二进制优化

把第 i i i种物品换成 p [ i ] p[i] p[i]件0-1背包中的物品,则得到了物品数为 ∑ i p [ i ] \sum_i p[i] ip[i]的01背包问题,直接求解,复杂度仍然是 O ( V ∗ ∑ i p [ i ] ) O(V*\sum_i p[i]) O(Vip[i])

但是我们期望将它转化为0-1背包问题之后能够像完全背包一样降低复杂度。

仍然考虑二进制的思想,我们考虑把第 i i i种物品换成若干件物品,使得原问题中第 i i i种物品可取的每种策略——取 0... p [ i ] 0...p[i] 0...p[i]件——均能等价于取若干件代换以后的物品。另外,取超过 p [ i ] p[i] p[i]件的策略必不能出现。

具体方法是:将第 i i i种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数。使这些系数分别为 1 , 2 , 4 , . . . , 2 k − 1 , p [ i ] − 2 k + 1 1,2,4,...,2k−1,p[i]-2^k+1 1,2,4,...,2k1,p[i]2k+1 k k k是满足 p [ i ] − 2 k + 1 > 0 p[i]−2^k+1>0 p[i]2k+1>0的最大整数。

例如,如果 p [ i ] p[i] p[i]为13,就将这种物品分成系数分别为1,2,4,6的四件物品。分成的这几件物品的系数和为 p [ i ] p[i] p[i],表明不可能取多于 p [ i ] p[i] p[i]件的第 i i i种物品。另外这种方法也能保证对于 0... p [ i ] 0...p[i] 0...p[i]间的每一个整数,均可以用若干个系数的和表示,这个证明可以分 0... 2 k − 1 0...2^{k}-1 0...2k1 2 k . . . p [ i ] 2^{k}...p[i] 2k...p[i]两段来分别讨论得出,并不难。这样就将第 i i i种物品分成了 O ( l o g ( p [ i ] ) ) O(log(p[i])) O(log(p[i]))种物品,将原问题转化为了复杂度为 O ( V ∗ ∑ i l o g ( p [ i ] ) ) O(V∗\sum_i log(p[i])) O(Vilog(p[i]))的0-1背包问题,是很大的二进制优化

int count = 0, vval[M], vol[M];
 // count存分解后的物品总数
 //vval存分解后的每件物品的价值
 //vvol存分解完成后每件物品的代价 
 
 //二进制分解 
for(int i=1;i<=n;i++)
{
	int num[i] = read(), val[i] = read(), vol[i] = read();
	for(int j=1;j<=num[i];j<<=1)
	{
		vva[count] = j*val[i];
		vol[count] = j*vol[i];
		count++;
		num[i] -= j; 
	}
	if(num[i]>0)
	{
		vval[count] = num[i]*val[i];
		vol[count] = num[i]*vol[i];
		count++;
	}
}

// 0-1背包问题
memset(dp, 0, sizeof(dp)) ;
for(int i=1;i<count;i++)
	for(int j=v;j>=vol[i];j--)
		dp[j] = max(dp[j], dp[j-vol[i]]+val[i]);

简化版本

int dp[M] = {};
for(int i=1;i<=n;i++)
{
	int vol = read(), val = read(), num = read();
	for(int k=1;k<num;num-=k,k<<=1)
		for(int j=v;j>=k*vol;--j)
			dp[j] = max(dp[j], dp[j-k*vol]+k*val);
	for(int j=v;j>=num*vol;--j)
		dp[j] = max(dp[j], dp[j-num*vol]+num*val);
}

单调队列优化

多重背包问题同样有O(VN)的算法。这个算法基于基本算法的状态转移方程,但应用单调队列的方法使每个状态的值可以以均摊O(1)的时间求解。

int dp[M];
for(int i=1;i<=n;i++) 
{
	int vol = read(), val = read(), num = read();
	for(int k=0;k<vol;k++)  // 枚举体积的余数 
	{
		int a[M], b[M], head = 0, tail = 0; // 下标,值,队头,队尾
		for(int j=k;j<=v;j+=vol) 
		{
			int y = dp[j]-j/vol*val; // 当前体积的贡献值
			while(head<tail && y>=b[r-1]) tail--; // 出队
			a[tail] = j;
			b[tail++] = y;
			while(a[head]<j-num*vol]) head++; // 入队 
			dp[j] = b[head]+j/vol*val;
		} 
	}
} 

这里,应先确保搞明白了单调队列,就是在区间移动时动态维护区间的最值

观察多重背包的转移方程:

f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − k ∗ v o l [ i ] ] + k ∗ v a l [ i ] ) f[i][j]=max(f[i−1][j],f[i−1][j−k∗vol[i]]+k∗val[i]) f[i][j]=max(f[i1][j],f[i1][jkvol[i]]+kval[i])

单调队列优化的主要思想就是分组更新,因为 v o l [ i ] vol[i] vol[i]是成倍增加的 f [ i − 1 ] [ j ] f[i−1][j] f[i1][j]只会更新 f [ i − 1 ] [ j + k ∗ w [ i ] ] f[i−1][j+k∗w[i]] f[i1][j+kw[i]](这里是从前往后看的,所以是+)。对于当前 v o l vol vol的体积,我们可以按照余数将它分为 v o l vol vol组,也就是 0... v o l − 1 0...vol−1 0...vol1. 并且,同一个剩余系的数在一组,比如在模3意义下,1,4,7,10是一组,2,5,8,11是一组,3,6,9,12是一组每组的转移是互不影响的,也就是单独转移

举个例子

f [ i ] [ 5 v o l ] = m a x ( f [ i − 1 ] [ 4 v o l ] + v a l , f [ i − 1 ] [ 3 v o l ] + 2 v a l , f [ i − 1 ] [ 2 v o l ] + 3 v a l , f [ i − 1 ] [ v o l ] + v a l , f [ i − 1 ] [ 0 ] + 5 v a l ) f[i][5vol] = max(f[i-1][4vol]+val, f[i-1][3vol]+2val, f[i-1][2vol]+3val, f[i-1][vol]+val, f[i-1][0]+5val) f[i][5vol]=max(f[i1][4vol]+val,f[i1][3vol]+2val,f[i1][2vol]+3val,f[i1][vol]+val,f[i1][0]+5val)

f [ i ] [ 4 v o l ] = m a x ( f [ i − 1 ] [ 3 v o l ] + v a l , f [ i − 1 ] [ 2 v o l ] + 2 v a l , f [ i − 1 ] [ v o l ] + 3 v a l , f [ i − 1 ] [ v o l ] + v a l , f [ i − 1 ] [ 0 ] + 4 v a l ) f[i][4vol] = max(f[i-1][3vol]+val, f[i-1][2vol]+2val, f[i-1][vol]+3val, f[i-1][vol]+val, f[i-1][0]+4val) f[i][4vol]=max(f[i1][3vol]+val,f[i1][2vol]+2val,f[i1][vol]+3val,f[i1][vol]+val,f[i1][0]+4val)

让所有 f [ i ] [ j ] f[i][j] f[i][j]都减去 j / v o l ∗ v a l j/vol*val j/volval

f [ i ] [ 5 v o l ] = m a x ( f [ i − 1 ] [ 4 v o l ] − 4 v a l , f [ i − 1 ] [ 3 v o l ] − 3 v o l , f [ i − 1 ] [ 2 v o l ] − 2 v a l , f [ i − 1 ] [ v o l ] − v a l , f [ i − 1 ] [ 0 ] ) f[i][5vol] = max(f[i-1][4vol]-4val, f[i-1][3vol]-3vol, f[i-1][2vol]-2val, f[i-1][vol]-val, f[i-1][0]) f[i][5vol]=max(f[i1][4vol]4val,f[i1][3vol]3vol,f[i1][2vol]2val,f[i1][vol]val,f[i1][0])

f [ i ] [ 4 v o l ] = m a x ( f [ i − 1 ] [ 3 v o l ] − 3 v a l , f [ i − 1 ] [ 2 v o l ] + 2 v a l , f [ i − 1 ] [ v o l ] − 2 v a l , f [ i − 1 ] [ v o l ] − v a l , f [ i − 1 ] [ 0 ] ) f[i][4vol] = max(f[i-1][3vol]-3val, f[i-1][2vol]+2val, f[i-1][vol]-2val, f[i-1][vol]-val, f[i-1][0]) f[i][4vol]=max(f[i1][3vol]3val,f[i1][2vol]+2val,f[i1][vol]2val,f[i1][vol]val,f[i1][0])

f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j   m o d   v o l + k ∗ v o l ] − k ∗ v a l + j ∗ v a l ) f[i][j]=max(f[i−1][j \ mod\ vol+k∗vol]−k∗val+j∗val) f[i][j]=max(f[i1][j mod vol+kvol]kval+jval)

j   m o d   w j\ mod\ w j mod w一定后,就可以用单调队列来优化了

2.1.4 混合背包

顾名思义,混合背包就是有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)

最直接的想法:二进制分解。可以取无限次物品我们就记其num=99999,这样同样可以利用二进制分解开来

void binary_split()
{
    for(int i=1;i<=n;i++)
    {
        int k = 1;
        while(num[i])
        {
            ccost[++tp] = k*volume[i];
            vvalue[tp] = k*value[i];
            num[i] -= k;
            k *= 2;
            if(num[i]<k)
            {
                ccost[++tp] = volume[i]*num[i];
                vvalue[tp] = value[i]*num[i];
                break;
            }
        }
    }
}

binary_split();
for(int i=1;i<=tp;i++)
    for(int j=ttime;j>=ccost[i];j--)
        dp[j] = max(dp[j], dp[j-ccost[i]]+vvalue[i]);

2.1.5 二维费用背包

给一个容量为V的背包,你的负重最大只有W,然后有n种物品,每种都有若干个(0个,无限,多个),体积为volume[i],重量为weight[i],价值为value[i].问最多能装多少价值的物品,在不超过体积及负重的情况下?

for(int i=1;i<=n;i++)
	for(int j=v;j>=volume[i];j--)
    	for(int k=w;k>=weight[i];k--)
        	dp[j][k]=max(dp[j][k],dp[j-volume[i]][k-weight[i]]+value[i]);

2.1.7 分组背包

有n件物品可以被放入一个容量为v的背包中,每件物品体积为volume[i],价值为value[i].此外,这些物品被分成p组,每组中的物品最多只能选一件,求背包中最多可以装多少价值的物品.

for(int k=1;k<=p;k++) 
	for(int j=v;j>=0;j–) //此处遍历顺序与物品种类有关 
		for(int i: part[k]) 
			dp[j]=max(dp[j],dp[j-volume[i]]+value[i]).

注意遍历方式一定是 k j i kji kji,如果是 k i j kij kij的话就无法保证每组只选一个了。

j j j i i i保证了每组内,每个体积只会被一个最优的物品访问到。

2.1.8 有依赖的背包问题

这种背包问题的物品间存在某种“依赖”的关系。也就是说,物品i依赖于
物品j,表示若选物品i,则必须选物品j。为了简化起见,我们先设没有某个物
品既依赖于别的物品,又被别的物品所依赖;另外,没有某件物品同时依赖多
件物品。

for(int i=1;i<=n;i++)
{
	memcpy(tmp,dp,sizeof(dp));
	for(each attachment k of item i)
		for(int j=v;j>=volume[k];j--)
			tmp[j]=max(tmp[j],tmp[j-volume[k]]+value[k]);
	for(int j=v;j>=volume[i];j--)
		dp[j]=max(dp[j],tmp[j-volume[i]]+value[i]);
}

2.1.9 泛化物品

泛化物品准确来说,不是一类题目,而是一种思想。泛化物品的定义是

考虑这样一种物品,它并没有固定的费用和价值,而是它的价值随着你分
配给它的费用而变化。这就是泛化物品的概念。

or

更严格的定义之。在背包容量为V 的背包问题中,泛化物品是一个定义
域为0 . . . V 中的整数的函数h,当分配给它的费用为v时,能得到的价值就
是h(v)。

or

这个定义有一点点抽象,另一种理解是一个泛化物品就是一个数组h[0 . . . V ],
给它费用v,可得到价值h[v]。

于是,

  • 一个0-1背包中的物品(体积 c i ci ci,价值 w i wi wi),它的泛化物品模型是 h ( c i ) = w i h(ci)=wi h(ci)=wi h ( 其 他 ) = 0 h(其他)=0 h()=0
  • 一个完全背包中的物品,它的模型是 h ( c i ∗ k ) = w i ∗ k h(ci*k)=wi*k h(cik)=wik,其中k为正整数且 c i ∗ k < = V ci*k<=V cik<=V h ( 其 他 ) = 0 h(其他)=0 h()=0
  • 一个多重背包中的物品,则是 h ( c i ∗ k ) = w i ∗ k h(ci*k)=wi*k h(cik)=wik,其中 k < = n i k<=ni k<=ni c i ∗ k < = V ci*k<=V cik<=V h ( 其 他 ) = 0 h(其他)=0 h()=0
  • 一个互斥的物品组, h ( c i ) = w i h(ci)=wi h(ci)=wi i i i取遍组中物品的编号, c i ci ci相同时 w i wi wi取最小值, h ( 其 他 ) = 0 h(其他)=0 h()=0

泛化物品的和

如果给定两个泛化物品 a a a b b b,现在有体积v来装这两种物品,要求获得最大价值,怎么做?

d p [ j ] = m a x ( a ( k ) + b ( v − k ) ) dp[j]=max(a(k)+b(v-k)) dp[j]=max(a(k)+b(vk)) k k k取遍 0 0 0 j j j,答案就是 d p [ v ] dp[v] dp[v]

新合成的dp数组,实际上,也是一个泛化物品。

由泛化物品的性质可知,如果将两个泛化物品这样合成一个新的物品,新的物品在问题中完全可以取代原有的两个物品。

2.1.10 背包问题问法的变化

记录具体的选择

如果需要记录具体的选择,可以另起一个 x[ ] 数组,x[i]=0表示不拿,x[i]=1表示拿。

f[n][c]为最优值,如果f[n][c]=f[n-1][c] ,说明有没有第n件物品都一样,则x[n]=0 ; 否则 x[n]=1。当x[n]=0时,由x[n-1][c]继续构造最优解;当x[n]=1时,则由x[n-1][c-w[i]]继续构造最优解。以此类推,可构造出所有的最优解。

for(int i=n;i>1;i--)
{
	if(dp[i][v]==dp[i-1][v]) x[i] = 0;
	else
	{
		x[i] = 1;
		v -= volume[i];
	}
	x[1] = (dp[1][c]>0)?1:0;
}

输出字典序最小的方案

字典序最小是指 N N N号物品的选择方案排列出来以后字典序最小

我们的做法是:先把物品逆序排列一下,然后按照前面的状态转移方程来求值。唯一要注意的是,如果 f [ i ] [ j ] = = f [ i − 1 ] [ i − j ] f [ i ] [ j ] f[i][j]==f[i−1][i−j]f[i][j] f[i][j]==f[i1][ij]f[i][j]以及 f [ i ] [ j ] = = f [ i − 1 ] [ j − w [ i ] ] + v [ i ] f[i][j]==f[i−1][j−w[i]]+v[i] f[i][j]==f[i1][jw[i]]+v[i]同时成立,应该按照后者(即选择了物品 i i i)输出

求方案总数

当我们要求装满背包或者将背包装至某一指定容量的方案总数时,一般只需要将转移方程的max改成sum即可。初始条件为 d p [ 0 ] [ 0 ] = 1 dp[0][0]=1 dp[0][0]=1

求最优方案的总数

这里与求方案总数不同的是我们要求所装的物品价值最大,于是,结合求最大总价值和方案总数两个问题的思路,最优方案的总数可以这样求: f [ i ] [ j ] f[i][j] f[i][j]意义同前述, g [ i ] [ j ] g[i][j] g[i][j]表示这个子问题的最优方案的总数,则在求 f [ i ] [ j ] f[i][j] f[i][j]的同时求 g [ i ] [ j ] g[i][j] g[i][j]

求次优解,第K优解

求次优解往往可以相同的复杂度解决,第K优解则比求最优解的复杂度上多一个系数K。其基本思想是将每个状态都表示成有序队列,将状态转移方程中的max/min转化成有序队列的合并。

这里只讲一下0-1背包的情况:

如果要求第K优解,那么状态 f [ i ] [ j ] f[i][j] f[i][j]就应该是一个大小为K的数组 f [ i ] [ j ] [ 1... K ] f[i][j][1...K] f[i][j][1...K]。其中 f [ i ] [ j ] [ k ] f[i][j][k] f[i][j][k]表示前i个物品、背包大小为j时,第k优解的值。“ f [ i ] [ j ] f[i][j] f[i][j]是一个大小为K的数组”这一句,熟悉C语言的同学可能比较好理解,或者也可以简单地理解为在原来的方程中加了一维。显然 f [ i ] [ j ] [ 1... K ] f[i][j][1...K] f[i][j][1...K]这K个数是由大到小排列的,所以我们把它认为是一个有序队列。然后原方程就可以解释为: f [ i ] [ j ] f[i][j] f[i][j]这个有序队列是由 f [ i − 1 ] [ j ] f[i−1][j] f[i1][j] f [ i − 1 ] [ j − w [ i ] ] + v [ i ] f[i−1][j−w[i]]+v[i] f[i1][jw[i]]+v[i]这两个有序队列合并得到的。有序队列f[i−1][j]f[i-1][j]f[i−1][j]即 f [ i − 1 ] [ j ] [ 1... K ] f[i−1][j][1...K] f[i1][j][1...K] f [ i − 1 ] [ j − w [ i ] ] + v [ i ] f[i−1][j−w[i]]+v[i] f[i1][jw[i]]+v[i]则理解为在 f [ i − 1 ] [ j − w [ i ] ] [ 1... K ] f[i−1][j−w[i]][1...K] f[i1][jw[i]][1...K]的每个数上加上 v [ i ] v[i] v[i]后得到的有序队列。合并这两个有序队列并将结果的前KKK项储存到 f [ i ] [ j ] [ 1... K ] f[i][j][1...K] f[i][j][1...K]中的复杂度是 O ( K ) O(K) O(K)。最后的答案是 f [ N ] [ V ] [ K ] f[N][V][K] f[N][V][K]。总的复杂度是 O ( V N K ) O(VNK) O(VNK)。为什么这个方法正确呢?实际上,一个正确的状态转移方程的求解过程遍历了所有可用的策略,也就覆盖了问题的所有方案。只不过由于是求最优解,所以其它在任何一个策略上达不到最优的方案都被忽略了。如果把每个状态表示成一个大小为KKK的数组,并在这个数组中有序的保存该状态可取到的前KKK个最优值。那么,对于任两个状态的max运算等价于两个由大到小的有序队列的合并。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值