1. 问题描述:
时间限制: 3.0s 内存限制: 512.0MB 本题总分:25 分
杂货铺老板一共有N件物品,每件物品具有ABC三种属性中的一种或多种。从杂货铺老板处购得一件物品需要支付相应的代价。现在你需要计算出如何购买物品,可以使得ABC三种属性中的每一种都在至少一件购买的物品中出现,并且支付的总代价最小。
【输入格式】
输入第一行包含一个整数N。
以下N行,每行包含一个整数C和一个只包含"ABC"的字符串,代表购得该物品的代价和其具有的属性。
【输出格式】
输出一个整数,代表最小的代价。如果无论如何凑不齐ABC三种属性,输出-1。
【样例输入】
5
10 A
9 BC
11 CA
4 A
5 B
【样例输出】
13
【评测用例规模与约定】
对于50%的评测用例,1 <= N <= 20
对于所有评测用例,1 <= N <= 1000, 1 <= C <= 100000
2. 思路分析:
① 一开始想到的是使用递归的方法,尝试所有可能的拼接方案(对于当前的字符串可以选择要还是不要两种平行状态,两个递归方法就可以解决)但是由于数据量为10^5应该会超时,感觉这道题目应该是可以使用动态规划的方法解决(暴力枚举的条件下进行优化),但是一开始没有找到一个比较好的办法来表示ABC三种属性,在csdn上看到一个使用二进制来表示ABC三种属性的方法,感觉这位老哥写得太棒了,思路使用的是二进制 + 动态规划的方法,其中二进制中的4/2/1分别表示CBA三种属性,这样就可以将ABC三种属性映射到下标中了,因为使用的是python语言所以可以声明一个长度为8的dp列表(相当于是java或者c、c++的数组),dp[i]表示的是具有i的属性花费的最小代价,一个数字i就是代表当前物品具有的属性,dp[i]的含义确定之后接下来的就好办了,对于当前的物品具有的属性,尝试添加到其他的物品中(循环遍历dp[1]~dp[7]),看属性与价值是否发生变化,如果发现能够构成的价值更小那么就更新添加属性之后的那个位置对应的值,dp状态转移方程也是不难想到的dp[v | j] = c + dp[j](c + dp[k] < dp[v | k]),c表示当前输入物品的价值,v表示当前输入的物品的属性,v | j表示尝试将当前的属性添加到具有j代表属性的那个位置,看是否能够构成代价更小的。
② 我们在遇到这种可以使用暴力枚举的题目如果数据量大的时候需要往动态规划这方面思考,看怎么样才可以进行进一步的优化,明确上一个状态与当前的状态之间可能通过什么样的方式进行推导,很多情况下都是一些组合问题的暴力,可能有多种组合方式可以得到当前的状态,我的目标是通过循环找出最优的那种状态,而且状态转移其实是一个很关键的步骤,大佬的参考博客
③ 除了使用上面的动态规划的思路解决之外,这里补上了递归的解法(只是为了记录一下思路虽然可能会超时)。其实这道问题有一个很明显的特点也就是我们需要在所有的组合方案中找出一个最佳的方案,这个很明显是递归可以解决的问题的特点,递归可以搜索所有的可能性然后在搜索的时候使用变量记录下历史最优的方案,递归结束之后变量记录的历史上的最优的方案就是最佳的方案,对于这道题目也是如此,我们无非需要在所有可能的组合方案中找到满足具有ABC属性并且这个组合方案花费的代价是最小的,所以使用递归搜索所有的可能性即可。递归方法解决的一个重要的问题就是确定递归的平行状态,说白了就是递归的方式,这道题目其实很容易想到递归的平行状态,对于当前的物品可以选择要还是不要,对于下一个物品也是如此,所以很明显递归的时候存在两种平行状态,对于两种平行状态选择要还是不要的递归可以选择两种写法。第一种是在递归的方法体内写两个递归方法,表示要还是不要,在方法中需要传递当前递归的位置index,下一次调用递归方法的时候那么就是index + 1了,这样在递归的时候这个变量就可以作为出口,也就是在递归的时候可以判断当前这个变量是否到达了列表(python)的最终的位置,如果到达了列表的最终位置那么就需要return了,除了这个参数之外还需要传递记录当前方案中已经拼接的字符串这样在递归的时候才可以判断当前递归的方案是否存在ABC三种属性,并且还需要传递一些从控制台输入的变量值,比如物品的属性、代价,因为使用的是python语言所以在输入的时候可以将当前物品的属性与代价作为一个元组添加到列表的属性中,这样在递归的时候取出列表中的元素会非常方便(写递归方法的时候我们是需要什么参数传递什么参数,一般是先传递递归出口与记录方案的参数,然然后有其他需要的参数再传递其他的参数)。第二种写法是在for循环中进行递归,其实这种写法与只写两个递归方法的本质是一样的,for循环的范围为当前递归的位置一直到列表的最终位置,其实与只写两个递归方法是一样的,对于循环到当前位置index的物品那么表示的就是选择这个物品对应的平行状态,当递归return返回之后进入下一次循环那么表示的就是不选择当前的物品,这样就可以一直递归下去,因为for循环本身就隐含着就有一个递归出口也就是for循环中循环的范围是在列表元素长度之内的,限制了递归的位置超出列表长度之后继续递归下去,所以我们可以不用写递归的出口。在递归的是我们选择的是直接拼接当前物品对应的字符串:s + abs[index][1],而不是直接对字符串进行修改:s += abs[index][1],因为如果修改了当前组合方案中的字符串那么就需要在递归调用完成之后进行回溯,也就是当前递归方法的后面进行回溯
3. 代码如下:
动态规划:
import sys
if __name__ == '__main__':
dp = [sys.maxsize] * 8
N = int(input())
for i in range(N):
# split函数的返回值就是一个列表
cur = input().split()
c = int(cur[0])
v = 0
# v记录当前物品具有的属性
for j in cur[1]:
# 左移这么多位表示在xxx对应的二进制位上将置为1表示存在这个属性,
v |= 1 << (ord(j) - ord("A"))
# 上面的循环结束之后v表示的就是当前输入物品具有的所有属性
# 长度为8的循环是为了尝试将当前的输入物品属性添加到之前的位置看是否能够构成代价更小的
for k in range(1, 8):
# v & k != v表示之前已经具有所有的属性这个时候添加进来一点贡献都没有
# c + dp[k] < dp[v | k]其中v | k表示将当前的物品添加到k这个位置对应的属性中看是否构成价值更小
# 或运算的意思是添加当前输入物品的属性到之前的物品属性中
if v & k != v and dp[k] != sys.maxsize and c + dp[k] < dp[v | k]:
dp[v | k] = c + dp[k]
# 取出dp[v]与c中的最小值的那个
dp[v] = min(dp[v], c)
# 输出最后一个元素表示具有三种属性的物品花费的最小价值
print(dp[7] if dp[7] != sys.maxsize else -1)
递归:
import sys
from typing import List
res = sys.maxsize
def dfs(s: str, abs: List[tuple], cost: int, index: int):
# 使用global关键字声明全局变量
global res
# 当ABC字符都存在于字符串s中那么就可以直接返回了
if "A" in s and "B" in s and "C" in s:
res = min(res, cost)
return
for i in range(index, len(abs)):
dfs(s + abs[i][1], abs, cost + abs[i][0], i + 1)
if __name__ == '__main__':
N = int(input())
abs = list()
for i in range(N):
cur = input().split()
# 将当前物品的属性与价值作为一个元组添加到列表中
abs.append((int(cur[0]), cur[1]))
dfs("", abs, 0, 0)
print(res)
import sys
from typing import List
res = sys.maxsize
def dfs(s: str, abs: List[tuple], cost: int, index: int):
global res
if "A" in s and "B" in s and "C" in s:
res = min(res, cost)
return
# 递归出口
if index == len(abs): return
# 选择要当前的物品
dfs(s + abs[index][1], abs, cost + abs[index][0], index + 1)
# 不要当前的物品
dfs(s, abs, cost, index + 1)
if __name__ == '__main__':
N = int(input())
abs = list()
for i in range(N):
cur = input().split()
abs.append((int(cur[0]), cur[1]))
dfs("", abs, 0, 0)
print(res)