程序员的算法趣题Q28: 社团活动的最优分配方案

目录

1. 问题描述

2. 解题分析

2.1 递推方程

3. 递归式实现

4. 动态规划实现


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 递推方程

        不管用递归的方式求解,还是用动态规划求解,都涉及以下几个关键步骤:

  1. 找出子问题
  2. 列出原问题与子问题的递推关系式(recurrence equation)
  3. 找出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不选择最后一项社团两种情况进行考虑):

  1. 如果不选择最后一项(I)社团à f(I-1,J)
  2. 如果选择最后一项(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全解

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

笨牛慢耕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值