腾讯----小Q的歌单
文章目录
一、题目描述
小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
输出例子1:
9
二、分析
方法一:组合
这道题比较好的处理方法也最容易理解的是用组合数来求解,说到组合数就很容易想到这道题和我们高中做的从两个盒子里取小球的排列组合题。
- 此题可以转换为这样一道数学题,
有两个盒子,一个盒子里装有3个红球,一个盒子里装有3个白球,红球代表2分,白球代表3分,则从两个盒子中任意拿球使其分数等于9的拿法有多少种
。 - 这样就会想如果拿了0个红球,白球有多少种拿法,如果拿了1个、2个、3个红球,白球各有多少种拿法。
- 再者,将球的数量和球的分数换成未知的量:即有两个盒子,一个盒子里装有X个红球,一个盒子里装有Y个白球,红球代表A分,白球代表B分,则从两个盒子中任意拿球使其分数等于K的拿法有多少种。
- 很显然就和面试题一样了,
可以想到假设拿了 i 个红球( i <= X),需要满足条件( i * A <= K : 分数不能超过K)&&(( K - i* A)% B == 0 :确保分数相加等于K,即剩下的歌单长度可以在B中组合出来) && (( K - i* A)/ B <= Y :不能超过白球的数目),将满足条件的结果相加起来就是最后的结果。
- 而当
满足条件后从各自的盒子里拿球就有不同的拿法
,是很典型的排列组合问题,对于这道题我们可以建一个二维数组来存这些组合数,行标代表排列组合公式的下标,列标代表排列组合公式的上标
- 可以直接看代码:
#include<iostream>
using namespace std;
int main()
{
int k,a,x,b,y;
long long int count = 0;
long long int c[101][101];
while(cin>>k)
{
cin>>a>>x>>b>>y;
//初始化c数组
//c[i][j]代表从i个里面选j个共有多少种选取方法
c[0][0] = 1;
for(int i = 1;i <= 100;i++)
{
c[i][0] = 1;
for(int j = 1;j <= 100;j++)
{
c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % 1000000007;
}
}
//开始计算
for(int i = 0;i <= k / a && i <= x;i++)
{
//如果选取i个a歌单的歌曲,那么剩下的歌曲时间长度为k - i * a
//如果满足剩下的时间可以在b中组合出来,并且需要b歌曲的数量
//小于给定的y才可以
//如果满足条件,count就等于在x个里选取i个A长度的歌的选法乘以
//在y个里选取(k - i * a) / b个B长度的歌的选法
//最后相加即可
if((k - i * a) % b == 0 && (k - i * a) / b <= y)
count = (count + (c[x][i] * c[y][(k - i * a) / b])
% 1000000007) % 1000000007;
}
cout<<count<<endl;
}
}
方法二:动规
这道题和背包问题类似。
1. 第一步要明确两点,「状态
」和「选择
」
先说状态,如何才能描述一个问题局面?只要给定几个可选的歌曲和一个长度为K的限制,就形成了一个背包问题,对不对?所以状态有两个,就是「歌曲组成的总长度K」和「可选择的歌」
。
再说选择,也很容易想到啊,对于每首歌,你能选择什么?选择就是「装进背包:算上这首歌的时长
」或者「不装进背包:不算这首歌的时长
」嘛。
明白了状态和选择,动态规划问题基本上就解决了,只要往这个框架套就完事儿了:
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 择优(选择1,选择2...)
2. 第二步要明确dp数组的定义
dp数组是什么?其实就是描述问题局面的一个数组。换句话说,我们刚才明确问题有什么「状态」,现在需要用dp数组把状态表示出来。
首先看看刚才找到的「状态」有两个,也就是说我们需要一个二维dp数组,一维表示可选择的歌曲,一维表示歌曲总时长。
dp[i][w]的定义如下:对于前i首歌曲,当前歌曲总时长为w,这种情况下组合歌曲的方法有dp[i][w]种
。
比如说,如果 dp[3][5] = 6,其含义为:对于给定的一系列歌曲中,若只对前 3 个歌曲进行选择,当歌曲总时长K为 5 时,最多的组合方法有6种。
根据这个定义,我们想求的最终答案就是dp[N][W]。base case 就是dp[0][…] = dp[…][0] = 0,因为没有歌曲或者歌曲总时长为0的时候,能构选择的组合方法就是 0种,不过我们认为当没有歌曲和歌曲总时长为0都满足的时候认为算一种方法,即dp[0][0] == 1
。
细化上面的框架:
int dp[N+1][W+1]
dp[0][..] = 0
dp[..][0] = 0
for i in [1..N]:
for w in [1..W]
把歌曲 i 装进背包,
不把歌曲 i 装进背包
)
return dp[N][W]
3. 第三步,根据「选择」,思考状态转移的逻辑
简单说就是,上面伪码中「把歌曲i装进背包」和「不把歌曲i装进背包」怎么用代码体现出来呢?
这一步要结合对dp数组的定义和我们的算法逻辑来分析:
先重申一下刚才我们的dp数组的定义:
dp[i][w]表示:对于前i首歌曲,当前总时长为w时,这种情况下可以组合的方法是dp[i][w]种。
如果你没有把这第i个歌曲装入背包,那么很显然,最大价值dp[i][w]应该等于dp[i−1][w]。你没有选择一首歌曲,总时长也就不会变,那就继承之前的结果。
如果你把这第i首歌曲装入了背包,那么dp[i][w]应该等于dp[i−1][w−wt[i−1]] + val[i−1]
。
首先,由于i是从 1 开始的,所以对val和wt的取值是i-1。
而dp[i−1][w−wt[i−1]]也很好理解:你如果想装第i首歌曲,你怎么计算这时候的组合方法?换句话说,在装第i首歌曲的前提下,总时长的最大组合方法是多少?
显然,你应该寻求剩余时长w−wt[i−1]限制下组合的方法,加上第i首歌曲的时长val[i−1],这就是装第i首歌曲的前提下,总时长的组合方法
。
4.到这里基本上一背包问题的方式解释了一遍本题,但是这道题是没有每个物品 的价值的,所以需要根据单首歌曲的时长构造这样一组价值信息
for(int i=1;i<=x;i++)
len[i]=a;
for(int i=x+1;i<=length;i++)
len[i]=b;
5.每首歌曲的‘价值’有了,那么在什么情况下选择加入歌曲,什么情况下不能加入歌曲??
lens[j]表示第j首歌的长度,如果当前剩余的歌曲时长 >= lens[j],那么由前j首歌组成长度为i的歌单的方法数可分为两部分,第一部分是dp[j - 1][i],即由前j-1首歌组成长度为i的歌单的方法数(不选取这首歌),第二部分是dp[j-1][i-lens[j]](选取这首歌)
。
6. 如果当前剩余的总时长 < lens[j],那么dp[i][j]等于dp[i][j−1]。
代码中dp[i][j]和上面描述有点不一样(手误):dp[i][j]代表用j首歌组成总时长i的方法有多少种,刚好写反了,不过道理是一样的
。
#include <iostream>
#include <algorithm>
#include<string.h>
#include<map>
#include<iterator>
#include<math.h>
using namespace std;
const int mod = 1000000007;
int k;//总时长
int a,x,b,y;//a代表a歌曲的长度,x代表a歌曲的数量...
int dp[1010][210];//dp数组
int len[210];//存储每首歌的时长
int main()
{
cin>>k;
cin>>a>>x>>b>>y;
int length = x + y;
memset(dp,0,sizeof(dp));
memset(len,0,sizeof(len));
//base case
dp[0][0] = 1;
//根据每首歌的时长构造‘价值’数组
for(int i = 1;i <= x;i++)
len[i] = a;
for(int i = x + 1;i <= length;i++)
len[i] = b;
//循环
for(int i = 0;i <= k;i++)
{
for(int j = 1;j <= length;j++)
{
//如果当前剩余歌曲的总时长大于单首歌曲的时长
//那么对于这首歌有可选和不选两种选择
//把组合的结果相加,都算满足情况
if(i >= len[j])
{
dp[i][j] = (dp[i][j - 1] + dp[i - len[j]][j - 1]) % mod;
}
//如果当前剩余歌曲的总时长小于单个个的时长
//代表不能选取这首歌,如果选取总时长就肯定会变长
//不满足情况
else
{
dp[i][j] = dp[i][j - 1] % mod;
}
}
}
cout<<dp[k][length]<<endl;
return 0;
}