华为机试:购物单问题(动态规划)

题目描述:
王强今天很开心,公司发给N元的年终奖。王强决定把年终奖用于购物,他把想买的物品分为两类:主件与附件,附件是从属于某个主件的,下表就是一些主件与附件的例子:

主件附件
电脑打印机,扫描仪
书柜图书
书桌台灯,文具
工作椅

如果要买归类为附件的物品,必须先买该附件所属的主件。每个主件可以有 0 个、 1 个或 2 个附件。附件不再有从属于自己的附件。王强想买的东西很多,为了不超出预算,他把每件物品规定了一个重要度,分为 5 等:用整数 1 ~ 5 表示,第 5 等最重要。他还从因特网上查到了每件物品的价格(都是 10 元的整数倍)。他希望在不超过 N 元(可以等于 N 元)的前提下,使每件物品的价格与重要度的乘积的总和最大。

设第 j 件物品的价格为 v[j] ,重要度为 w[j] ,共选中了 k 件物品,编号依次为 j 1 , j 2 ,……, j k ,则所求的总和为:

v[j 1 ]*w[j 1 ]+v[j 2 ]*w[j 2 ]+ … +v[j k ]*w[j k ] 。(其中 * 为乘号)

请你帮助王强设计一个满足要求的购物单。

输入描述:

输入的第 1 行,为两个正整数,用一个空格隔开:N m
(其中 N ( <32000 )表示总钱数, m ( <60 )为希望购买物品的个数。)
从第 2 行到第 m+1 行,第 j 行给出了编号为 j-1 的物品的基本数据,每行有 3 个非负整数 v p q
(其中 v 表示该物品的价格( v<10000 ), p 表示该物品的重要度( 1 ~ 5 ), q 表示该物品是主件还是附件。如果 q=0 ,表示该物品为主件,如果 q>0 ,表示该物品为附件, q 是所属主件的编号)

输出描述:

输出文件只有一个正整数,为不超过总钱数的物品的价格与重要度乘积的总和的最大值( <200000 )。

示例1:
输入:

1000 5
800 2 0
400 5 1
300 5 1
400 3 0
500 2 0

输出:

2200


题目分析:
首先,附件必须在买了主件之后才能购买,为了降低题目的复杂度,可以把附件都合并到主件中,与主件一同判断。
其次,题目是希望取得分数的最高值,那么购买当前主件所获得的最高分,就等于 还没买当前主件时获得的最高分 加上 当前主件的分值。
(上一句可能有些绕,例如目前已经花了100元,当前主件是20元,重要度是2,那么主件的分值就是40。那么买这个主件之前是花了80元,如果花了80元时获得的最高分加上40就是花了100元时的最高分,那么就更新100元时的最高分。)

按照这个逻辑,我们要求的就是已经花了N元时的最高分,再一步步往回推花了比N元少的时候的最高分。
所以解决方案中我们会设定一个列表 f ,f[i] 代表花了 i 元时的最高分(0 <= i <= N),这个列表用于实现动态规划。

定好了动态规划的大致方向后,其实购买主件是有五种情况的:不买主件、只买主件、主附1、主附2、主附12。
同样的,也是判断在花了当前 f[i] 这么多钱后,哪种情况的分数最高,即代码中的 max_,最终再填回 f[i] 。

大致思路如上了,后面的细节再通过代码注释解释:

class Good(object):
    def __init__(self, v, vp):
        self.v = v  # 价格
        self.vp = vp  # 价格 * 重要度
    
    # 便于调试
    def __str__(self):
        return "{} {}".format(self.v, self.vp)
    
    def __repr__(self):
        return self.__str__()

if __name__ == "__main__":
    tmp = input()
    N, m = tmp.split()
    # 商品价格都是10的倍数,因此个位数的总钱数是没用的
    # 对总钱数和商品价格都除以10,可以加速
    N, m = int(N) // 10, int(m)
    # goods[0]是不用的,只用goods[1] ~ goods[m]
    # 因为题目给的 q 也是从 1 开始
    goods = [[None for _ in range(3)] for _ in range(m + 1)]
    for i in range(1, m + 1):  # 注意要从1到m
        v, p, q = input().split()
        v, p, q = int(v) // 10, int(p), int(q)
        tmp = Good(v, v * p)
        # 0 放主件,1 和 2 分别放两个附件
        if q == 0:
            goods[i][0] = tmp
        elif goods[q][1] is None:
            goods[q][1] = tmp
        else:
            goods[q][2] = tmp
            
    f = [0 for _ in range(N + 1)]  # f[j]代表花了j元时最高的分数
    for i in range(1, m + 1):
        if goods[i][0] is None: continue  # 如果是附件,就跳过
        for j in range(N, -1, -1):  # 从N到0
            # N要倒叙遍历是因为f[j]要基于比j少钱的结果来计算
            # 而同一个商品不能买两次
            # 所以倒叙能避免重复买的问题
            # 举个正序的反例:
            # 例如目前循环到商品3,价格是20
            # 在j=20时,足够钱买商品3
            # 遍历到j=40时,依然足够钱买商品3,就会重复购买
            # 假如是倒叙,他们所比较的情况都是这个商品还没买过的情况
            # 所以倒叙才是OK的
            master = goods[i][0]
            max_ = f[j]
            # 如果够钱,且买这master之前的最大值(f[j - master.v])加上当前master的价值
            # 比现在的总价值高,则更新总价值max_
            if j >= master.v and max_ < f[j - master.v] + master.vp:
                max_ = f[j - master.v] + master.vp

            if goods[i][1] is not None:  # 如果有附件1
                vt = master.v + goods[i][1].v  # 主件和附件1的总价,t=total
                vpt = master.vp + goods[i][1].vp  # 总价值
                # 同样的,如果比之前大,就更新总价值
                if j >= vt and max_ < f[j - vt] + vpt:
                    max_ = f[j - vt] + vpt
            # 如果有附件2,有两种情况
            # 主件加附件2
            # 主件加附件1加附件2
            if goods[i][2] is not None:
                vt = master.v + goods[i][2].v
                vpt = master.vp + goods[i][2].vp
                if j >= vt and max_ < f[j - vt] + vpt:
                    max_ = f[j - vt] + vpt
                    
                vt = master.v + goods[i][1].v + goods[i][2].v
                vpt = master.vp + goods[i][1].vp + goods[i][2].vp
                if j >= vt and max_ < f[j - vt] + vpt:
                    max_ = f[j - vt] + vpt
            f[j] = max_  # 最终max_就会是五种情况中的最大值
	print(f[N] * 10)

至此结束了,欢迎留言沟通~

参考来源:
讨论区的高赞java实现:
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值