有点绕的dp问题

波动数列

https://www.acwing.com/problem/content/1216/

问题分析:

先明确题目所求:求满足条件的总方案数

需要满足的条件是:总长度为n,总和为s,后一项总是比前一项增加a或者减少b。

综合的来看,求的是方案数,又是很明显的数列运算,外加上很大的数据范围,dp无疑

1≤n≤1000,
−10^9 ≤s≤10^9,
1≤a,b≤10^6

既然是dp,就得走流程。

在这之前,看到数列,总得分析一下这个数列:

每一项的规律

a[1] = a[1], a[2] = a[1] + d1, a[3] = a[1] + d1 + d2, ...

其中di代表第i项的选择,即+a或者-b

通项和

s = a[1] + a[2] + ... + a[n]

相对来说,每一项都有a[1], 并且a[1]是不确定的,想要遍历所有的a[1] 是不现实的,况且每一个a[1]对应的数列是不唯一的,但每一种数列都会唯一的产生一个a[1],很容易想到提取a[1],即

a[1] = {s - {0 + (a[2] - a[1]) + ... + (a[n] - a[1])}}

n * a[1] = s - {d2 + (d2 + d3) + ... + (d2 + d3 + ... + dn)}

进一步看,

a[1] * n = s - [(n - 1)*d2 + (n - 2)d3 + ... + dn]

显然,右边的两项需要模n同余,所以,我们成功的把一个混乱的通项和变成了一个有规律的,可以逐步递加的一个式子,并且不必考虑a[1]是啥。我看网上有一部分同学都卡在了同一个位置-----就是不能很好的将a[i]的每一项分离开,进行降维打击。只要理解了上面式子的含义,这个问题就成功的解决了。总结的说我们接下来要做的就是在每一次循环中解决一个d,比如说第一次循环就把(n - 1)d2记录下来,而不是只记录一个d2.

(这里有点绕,多琢磨一下就出来了,这也是一部分题解没有说明白的地方——到底是对{a[n]}运算还是对(n - 1)*d2 + (n - 2)d3 + ... + dn进行运算)

接下来走个流程,把问题重新用dp的方法过一遍

第一步 :状态分析
  1. f(i)表示要选完第i个的方案数,但这是有意义不明的,到底是否满足同余关系,所以需要再来一维

  2. f(i,j)表示s[i] mod n = j,即

    (n - 1)*d2 + (n - 2)d3 + ... + (n - i + 1)di

    模n余数为j的所有方案数

第二步: 状态计算

走到了第i个位置,现在面临着加a还是减b的问题,现在的状态是f(i, j)

(1)加a

现在已经有第i - 1位置的情况:

(n - 1)*d2 + (n - 2)d3 + ... + (n - i + 2)d(i-1)

所以需要满足

((n - 1)*d2 + (n - 2)d3 + ... + (n - i + 2) * d(n-1) + (n - i + 1) * a ) == j\ \ mod \ \ (n)

可以看到在di位置的系数是(n - i + 1),那么也就是说如果第i步要加(n - i + 1)个a,那么就能得出i-1位置的状态

((n - 1)*d2 + (n - 2)d3 + ... + (n - i + 2) * d(n-1) ) == {j - (n - i + 1) * a}\ \ mod \ \ (n)

用集合的方式表示就是

f(i, j) = f(i - 1, (j - (n - i + 1) * a) % n)

(2)减b,通过上述分析有

((n - 1)*d2 + (n - 2)d3 + ... + (n - i + 2) * d(n-1) ) == {j + (n - i + 1) * b}\ \ mod \ \ (n)

f(i, j) = f(i - 1, (j + (n - i + 1)*b) % n)

综上,

f(i, j) = f(i - 1, (j - a) % x) + f(i - 1, (j + b) % x)

最终的结果就是f(n)

到这里,这个题的核心就被搞清楚了,接下来就是代码部分了。

上代码:
#include<iostream>

using namespace std;

const int N = 1010;
const int M = 100000007;
int f[N][N];

int mod(int a, int b)
{
    
    return (a % b + b)% b;
}

int main()
{
    int n, s, a, b;
    cin >> n >> s >> a >> b;
    f[0][0] = 1;//边界问题,如果一个数都没有的话,余数肯定是0,这是一个方案
    for(int i = 1; i < n; i ++)//由于不需要考虑首项,所以循环n - 1次就可以了
        for(int j = 0; j < n; j ++)
            f[i][j] = (f[i - 1][mod((j - (n - i)* a), n)] + f[i - 1][mod((j + (n - i)* b), n)])% M;
    cout << f[n - 1][mod(s,n)];
    return 0;
}
注意:

1.边界问题,在刚开始的时候,一个数都没计算,即处于a[1]状态,这时候的方案有且只有一个,所以需要给f(0, 0)赋值为1

2.这里有一个需要注意的地方,在c++里,负数取余会得到负数

cout << -3 % 5;
cout << -3 % -5;

这里得出的结果都是-3,所以mod函数绝对不是脱了裤子放屁,但是在python里就不需要这么做了。

posted @ 2021-02-26 02:17  kanbujian55  阅读(47)  评论(0)    收藏  举报