目录
1. 问题描述
某新建学校的校长,要为学生规划要为他们准备哪些社团活动。学校里有150名想要运动的学生,共有10种社团,每个社团所需要的活动场地面积和人数如下所示:
在总人数上限为150人的条件下,选择成立哪些社团,能够使得所需场地面积最大。
当然本问题的求解目标有点反直觉。一般的优化目标难道不应该是在场地面积的约束条件下最大化能够容纳的人数嘛。。。不过这个无所谓。
2. 解题分析
每个社团要么选择、要么不选择,在某个约束条件下,达成另外一个指标的最大化,这是一个经典的0/1背包问题(0/1-knapsack problem, or knapsack problem w/0 repetition)。
经典的背包问题的叙述中有以下几个要素,以及本题中各中要素的映射关系如下所示:
解决背包问题的标准框架当然是动态规划(Dynamic Programming)。动态规划的根本要点是一个大的问题可以分解为性质相同但是“规模”较小的子问题,也即原问题具有良好的子问题分解结构。这种问题的最基本的解决方案是递归方法,但是递归调用的代价比较大,而动态规划则可以看作是解决递归问题的一种优化方法。
- What Is Dynamic Programming? Simply put, dynamic programming is an optimization method for recursive algorithms, most of which are used to solve computing or mathematical problems. You can also call it an algorithmic technique for solving an optimization problem by breaking it into simpler sub-problems.
2.1 递推方程
不管用递归的方式求解,还是用动态规划求解,都涉及以下几个关键步骤:
- 找出子问题
- 列出原问题与子问题的递推关系式(recurrence equation)
- 找出baseline cases并求解, as the initial or boundary condition for solving the above-mentioned recurrence equation
令areas表示各项目所需场地面积数组,humans表示各项目的人数数组,maxHuman表示总人数限制,数组下标对应以上项目表中各项目的序号(counting from 1 – 为了叙述的方便取从1开始计数)。
以f(I,J)表示在候选社团为前I个社团{1,2,…,I},最大允许人数为J的条件下的解。则本问题的子问题可以理解{可选社团项更少and/or最大允许人数更小}的求解问题。如下我们可以求得本问题解的递推关系式(分考虑选择or不选择最后一项社团两种情况进行考虑):
- 如果不选择最后一项(I)社团à f(I-1,J)
- 如果选择最后一项(I)社团 à areas[I] + f(I-1,J-humans[I])
而本题要求解最大面积,所以要在以上两种选择中取较大的那个作为正确选择,即:
当然,以上还漏了一点,即当社团I的人数已经超过总人数限制J的话,事实上就只有(1)可以选择了,因此以上递推关系式需修正为:
接下来,需要考虑初始化(或者说边界条件)。很显然,当最大允许人数等于0,或者可选社团项集合为空时,所得的最大面积为0。即:
3. 递归式实现
本问题求解复杂度较低,采用递归方式的实现加上memoization可以在几乎可以忽略的时间中求解。实现代码如下所示:
# -*- coding: utf-8 -*-
"""
Created on Tue Aug 24 13:03:13 2021
@author: chenxy
"""
import sys
import time
# import random
# from typing import List
# from queue import Queue
# from collections import deque
class Solution:
def maxValueRecursion(self, values, costs, maxCost) -> int:
"""
:values: The area required by each club, assuming [0] is a dummy placeholder
:costs: Number of people in each club, assuming [0] is a dummy placeholder
:maxHumans: The maximum number of people allowed
:ret: The maximum area needed
"""
memo = dict()
def V(i,j):
"""
i: Up to the i-th club (1,2,...,i) are taken into consideration
j: Maximum allowed number of people
ret: The maximum area required
"""
# Baseline case
if j <= 0 or i <= 0:
return 0
if (i,j) in memo:
return memo[(i,j)]
# Recursive equation
if costs[i] <= j:
tmp = max(V(i-1,j), values[i] + V(i-1,j-costs[i]))
else:
tmp = V(i-1,j)
memo[(i,j)] = tmp
# print('V({0},{1}) = {2}'.format(i,j,tmp))
return tmp
return V(len(values)-1,maxCost)
if __name__ == '__main__':
sln = Solution()
tStart = time.time()
areas = [0, 11000, 8000, 400, 800, 900, 1800, 1000, 7000, 100, 300]
humans = [0, 40, 30, 24, 20, 14, 16, 15, 40, 10, 12]
maxHumans = 150
ans = sln.maxValueRecursion(areas, humans, maxHumans)
tCost = time.time() - tStart
print('areas = {0}'.format(areas))
print('humans = {0}'.format(humans))
print('maxHumans = {0}, maxArea = {1}, tCost = {2}(sec)'.format(maxHumans, ans, tCost))
运行结果:maxHumans = 150, maxArea = 28800, tCost = 0.0(sec)
注意,以上实现中,故意让传入数组areas和humans的第1项为0(as a dummy placeholder),只是为了与解说中的"coungting from 1"的叙述方式相连贯一致(hence, better readability)而已,没有什么必然性。
另外,以上函数的接口用了更通用的名称:values, costs, maxCost,这意味着它可以作为一个"0/1背包问题"的递归解法的通用代码。只要将实际问题的各种元素如上面所述那样映射到values, costs和maxCost上,即可直接调用该函数进行求解。
4. 动态规划实现
To be added.
上一篇:Q27: 禁止右转
下一篇:Q29: 合成电阻的黄金分割比
本系列总目录参见:程序员的算法趣题:详细分析和Python全解