最近开始多看一看名企曾经出过的编程题。今天看了一个动态规划的题,非常受用。题目从牛客上摘一下。
链接:https://www.nowcoder.com/questionTerminal/f3ab6fe72af34b71a2fd1d83304cbbb3
来源:牛客网
小Q有X首长度为A的不同的歌和Y首长度为B的不同的歌,现在小Q想用这些歌组成一个总长度正好为K的歌单,每首歌最多只能在歌单中出现一次,在不考虑歌单内歌曲的先后顺序的情况下,请问有多少种组成歌单的方法。
输入描述:
每个输入包含一个测试用例。 每个测试用例的第一行包含一个整数,表示歌单的总长度K(1<=K<=1000)。 接下来的一行包含四个正整数,分别表示歌的第一种长度A(A<=10)和数量X(X<=100)以及歌的第二种长度B(B<=10)和数量Y(Y<=100)。保证A不等于B。
输出描述:
输出一个整数,表示组成歌单的方法取模。因为答案可能会很大,输出对1000000007取模的结果。
示例1
输入
5 2 3 3 3
输出
9
一道典型的动态规划问题。一开始想用左老师的换钱方法数问题套用,但是这里的问题是规定了两种歌的数量上限,自己写出来的是不限次数且同类中各个元素都一样的版本。看了牛客一个C++的帖子以后豁然开朗,写了一个python版本的,示例过了。至少把思路留一下:
K = int(input())
[A, X, B, Y] = list(map(int, input().split(' ')))
if X == 0 and Y == 0:
print(1)
mod = 1000000007
dp = [[0 for col in range(K+1)] for row in range(X+Y+1)]
p = [0 for col in range(X+Y+1)]
for i in range(1, X+1):
p[i] = A
for i in range(X+1, X+Y+1):
p[i] = B
dp[0][0] = 1 # 初始化第一行,在没有纸币的时候,形成总和为0的有1种办法(就是都不取);总和为其他数值的,都无法达到,有0种办法
for i in range(1, K+1):
dp[0][i] = 0
for i in range(1, X+Y+1):
for j in range(0, K+1):
if j >= p[i]: # 当前要凑的这个数额j,可以由至少一个p[i]来完成
dp[i][j] = (dp[i-1][j] + dp[i-1][j-p[i]]) % mod # 两部分:第一部分,是指本行(i)一个都不取,照搬i-1时的方法数;
# 第二部分,指的是本行多用一个,则i-1行少用一个时的方法数
else:
dp[i][j] = dp[i-1][j] % mod
print(dp[X+Y][K])
关键点是dp二维列表的含义。每一行代表用的歌的个数,而它对应的长度在p列表里保存,也就是说有一个简单的对应关系(或者说不同的歌代表不同的行)。dp的每一列代表能凑成多长的总长。因此,行是X+Y+1行,列是K+1行,最后取的时候找dp[X+Y][K]就是要的答案了。
在这种动态规划外,一开始还看到网上普遍用的一种方法。这个矩阵构造得让我觉得有问题,不是动态规划的矩阵,到现在我也觉得这不像是dp方法。方法重点是利用两种歌其中一个的范围,用全排列的公式将所有相乘的结果相加。而全排列,是构造了杨辉三角矩阵,再查询这个矩阵得来的。多数网上方法都说这个也是动态规划,我不是很认同。一种形式的python代码如下:
# -*- coding:utf-8 -*-
mod = 1000000007
K = int(input())
A, X, B, Y = list(map(int, input().split()))
arr = [[0 for i in range(101)] for j in range(101)]
arr[0][0] = 1
for i in range(1,101):
arr[i][0] = 1
for j in range(1,101):
arr[i][j] = (arr[i-1][j-1]+arr[i-1][j]) % mod
ans = 0
for i in range(X+1):
if (i*A <= K and (K-A*i) % B == 0 and (K-A*i)//B <= Y):
ans = (ans +(arr[X][i]*arr[Y][(K-A*i)//B]) % mod) % mod
print(ans)
其中在最后输出结果的时候,就是同时卡了几个条件,恰好能凑成K,并且两种歌的数量不超限而已。方法是很好很巧妙的,可以学习借鉴一下,不过这真是dp吗?还望懂行的大佬指教一二。