Description
观察这个数列:
1 3 0 2 -1 1 -2 …
这个数列中后一项总是比前一项增加2或者减少3。
栋栋对这种数列很好奇,他想知道长度为 n 和为 s 而且后一项总是比前一项增加a或者减少b的整数数列可能有多少种呢?
Input
输入的第一行包含四个整数 n s a b,含义如前面说述。
Output
输出一行,包含一个整数,表示满足条件的方案数。由于这个数很大,请输出方案数除以100000007的余数。
Sample Input
4 10 2 3
Sample Output
2
参考程序
(方法1)枚举求解。
n,s,a,b=map(int,input().split())
def Full(ls,n):#生成一个n位二进制数序列
if len(ls)<n:
ls="0"*(n-len(ls))+ls
return ls
cnt=0
for i in range(2**(n-1)):
ls=bin(i)#转换为二进制形式(含有0b前导)
ls=ls[2:]
ls=Full(ls,n-1)
last=[]#记录操作序列,+a或是-b操作
temp_s=0
for j in ls:
if j=="0":
last.append(a)
else:
last.append(-b)
for t in range(len(last)):
temp_s=temp_s+sum(last[:t+1])
if (s-temp_s)%n==0:
cnt+=1
print(cnt%100000007)
这种算法思路很简单,对某个数做n次操作,每次操作不是+a,就是-b,刚好和二进制的0-1相匹配。所以可以一次性生成所有可能的操作序列(也就是0~2^n种),由0-1组成的位串。然后逐一进行检查,将各种操作叠加,反推第一项是否为整数,如果是整数则满足题意。这样的做法必然超时,算法的时间复杂度是O(2 ^ n),只通过了20%的测试用例。
(方法2)动态规划
#include <stdio.h>
#include <stdio.h>
#define LEN 1000
#define MOD 100000007
int main()
{
long long n,s,a,b,i,j;
int dp[2][LEN*LEN+5],ans=0;
scanf("%lld%lld%lld%lld",&n,&s,&a,&b);
dp[0][0]=1;
for(i=1;i<n;i++)
{
for(j=0;j<=i*(i+1)/2;j++)
{
if(i>j)
{
dp[i%2][j]=dp[(i-1)%2][j];
}
else
{//i<=j
dp[i%2][j]=(dp[(i-1)%2][j-i]+dp[(i-1)%2][j])%MOD;
}
}
}
long long total_a_b=n*(n-1)/2,try_cnt_a;
for(try_cnt_a=0;try_cnt_a<=total_a_b;try_cnt_a++)
{
if( (s-try_cnt_a*a+(total_a_b-try_cnt_a)*b)%n==0 )
{//枚举出了一个try_cnt_a(+a操作的总次数)
ans=(ans+dp[(n-1)%2][try_cnt_a])%MOD;
//查表求出实现try_cnt_a个+a操作的方案数
}
}
printf("%d\n",ans);
return 0;
}
上面的做法我参考了另一篇博客,学习了他的思路(链接:蓝桥杯 历届试题 波动数列 DP 01背包 滚动数组)如果不能理解我的分析还请移步去看下大佬的讲解~
- 设首项为x,并将每一步所做的操作记为P(+a 或者-b),因此结果可以表示为
s=x+(x+P)+(x+2P)+…+[x+(n-1)P]=n×x+
[P+2P+3P+…+(n-1)P]=n×x+P×n×(n-1)/2.从这个式子可以看出,操作P的总数是固定的,一共有n*(n-1)/2次,但这个和是多少还不能直接看出,因为这里边混合了若干个+a和-b;所以我们一旦确定总共的+a操作,-b操作也就随之确定,因而Pn(n-1)/2值也就确定了。再解上面的方程,如果存在整数x使得方程成立,则就存在一组操作序列。所以现在的一个目标就是求出+a(或者-b)操作的个数,这个可以用枚举实现,也就是从0(也就是只有-b,没有+a的极端情况)开始试探,到n*(n-1)/2(也就是只有+a,没有-b的极端情况)为止,容易分析这样算法的时间复杂度是O(n^2),比刚才的指数级时间复杂度要靠谱一点点。 - 0,P,2P,3P,…的另一种理解:上面由于把每个P看做或者是+a或者是-b,那么nP里混合了+a和-b,总有种不便于计算的感觉,可不可以让nP纯粹的理解为全是+a(或者-b)这一种操作呢,答案是肯定的。如下图所示,给出了几行操作示例。
前面理解的P是从上到下一行一行竖着看的,如果将视角转变为从右向左,就可以看出:1个-b,2个+a,3个-b,4个+a,5个+a,……找到规律了,是0,Q,2Q,3Q…。因此可以将让nP纯粹的理解为全是+a(或者-b),在后面我们就可以看到这样假设的便捷性。 - 刚才确定了+a的总操作次数,即为Pa,而且也可以认为nP是n个纯粹的+a(或者-b)操作,但是我们看到Pa不是一蹴而就的,而是从P,2P,3P,…,(n-1)P这些汇总而来的,因此必须要将总操作次数Pa拆分,搞清楚它是来自于P,2P,3P,…,(n-1)P的哪些项。举个例子,令a=1,Pa=7,那么得到Pa=7可以是选择P,6P这两项令其为+a,也可以选择3P,4P,也可以选择P,2P,4P。所以这就是一个子集问题。
- 设i表示前i个P操作,j表示前i个P操作里+a操作总个数,二维数组dp[i][j]就表示实现这个结果的方案数。先从边界情况入手:如果j=0,为了达到前i个P操作加起来总共有0个+a,只有一种方案,就是0P,也就是dp[i][0]=1;如果i=0,(j≥1)意味着0个P操作,不可能出来正整数个+a,0个方案dp[0][j]=0(j≥1).考虑其他情况:若j<i,P操作序列是0,P,2P,3P,…jP,(j+1)P,…iP,为了能构成j个+a,完全不需要选择后面(j+1)P,…iP,贡献+a,(注:这里的选择不能把一项拆开,比如3P不能从中只选择两个+a,因为在上一段已经发现规律了,可以将nP看成纯粹的+a或-b操作了,后面可以看到这样的假设将为问题的求解带来很大的方便!)。所以j<i时,dp[i][j]=dp[i-1][j];如果j≥i,意味着可能要从前i个操作里选取若干个。为了递推缩小规模,只考虑iP选不选:如果iP选,那么剩下的(j-i)个+a就要在前j-i个( 0,P,2P,3P,…(j-i)P )序列里面选。如果iP不选,那么j个+a就要从前i-1个( 0,P,2P,3P,…(i-1)P )里面选。所以dp[i][j]=dp[i-1][j-i]+dp[i-1][j];
- 优化:从递推方程例可以看出,第i行表格之和上一行某个位置有联系,因此我们只需要关注两行即可,不必设置n行,否则会内存超限。也就是交替使用第0行,第1行,第0行,第1行……也就是滚动数组这一结构,节省空间使用。
- 其他注意事项:大数处理,涉及n^2都最好用long long型,读入用scanf("%lld"……),否则会结果出错
个人感觉这道题真的有点难度,完全想不到后面的动态规划解法(感谢CSDN博主colorfulshark的分享!!)而且觉得自己“读懂了”人家的分析,自己动手写也遇到了各种bug(循环次数、0-1交替、大数的声明和读取等等)。算法题精妙的地方正是在于推动我们在编写程序时,对时间性能、空间性能做出极致的优化!debug的过程是痛苦,但读懂别人更好的思路,能学到知识确实是快乐的!