• CF1204E Natasha, Sasha and the Prefix Sums——DP/数学(组合数)


    题面

      CF1204E

    解析

       题意就是要求所有由$n$个$1$、$m$个$-1$构成的序列的最大前缀和的和

     算法一$(DP)$

       $n$, $m$都小于等于$2000$, 显然可以$DP$

      设$dp[i][j]$表示由$i$个$1$, $j$个$-1$构成的序列的最大前缀和的和

      $i$个$1$, $j$个$-1$构成的序列, 可以看做是在$i-1$个$1$, $j$个$-1$的序列的最前面加一个$1$得到,也可以看做是在$i$个$1$, $j-1$个$-1$的序列最前面加一个$-1$得到

      这也就意味着,$dp[i][j]$可以由$dp[i-1][j]$与$dp[i][j-1]$转移过来

      先考虑$dp[i-1][j]$对$dp[i][j]$的贡献,对于任意一种$i-1$个$1$, $j$个$-1$的序列, 在其首端加入一个$1$后,最大前缀和都会加$1$, 序列的个数为$C(i+j-1, j)$,因此$dp[i][j] += dp[i-1][j] + 1 * C(i-j+1, j)$

      在考虑$dp[i][j-1]$对$dp[i][j]$的贡献,这个和上面那个不一样,要麻烦一点。因为对于最大前缀和为$0$的序列,在其首端加入一个$-1$后,其最大前缀和仍然是$0$, 所以$dp[i][j] += dp[i][j-1] - 1 * (C(i-j+1, j-1) - f[i][j-1])$, 其中数组$f[i][j]$表示由$i$个$1$, $j$个$-1$构成的所有序列中, 最大前缀和等于$0$的序列个数

      因此问题变成了如何求f数组

      与求$dp$数组的思维过程类似。 $i$个$1$, $j$个$-1$构成的序列, 可以看做是在$i-1$个$1$, $j$个$-1$的序列的最后面(注意这里是后面,而$dp$数组是在前面)加一个$1$得到,也可以看做是在$i$个$1$, $j-1$个$-1$的序列最后面加一个$-1$得到

      对于$f$数组,显然有$i leqslant j$,那么无论在序列末尾插入$1$或$-1$,  原来最大前缀和为$0$的序列在插入$1$或$-1$后,其最大前缀和依然为$0$,因此$f[i][j] = f[i-1][j] + f[i][j-1]$

      状态转移方程大概就是这样了

      初始化:

      $f[0][i] = 1 (1 leqslant i leqslant m)$, 其余为$0$

      $dp[i][0] = i (1 leqslant i leqslant n)$, 其余为$0$

      答案就是$dp[n][m]$

      时间复杂度$O(NM)$

      在考试中我也定义出来了$dp$, $f$数组, 但无论我怎么想就是想不出来转移方程, 结果方程也并不复杂, $dp$还得多加练习啊

     代码:

    #include<cstdio>
    #include<cstring>
    #include<iostream>
    #include<algorithm>
    using namespace std;
    typedef long long ll;
    const int maxn = 2004, mod = 998244853;
    
    int n, m;
    ll dp[maxn][maxn], f[maxn][maxn], C[maxn<<1][maxn<<1];
    
    void init()
    {
        C[0][0] = 1LL;
        for(int i = 1; i <= n + m; ++i)
        {
            C[i][0] = 1LL;
            for(int j = 1; j <= i; ++j)
                C[i][j] = (C[i-1][j-1] + C[i-1][j]) % mod;
        }
    }
    
    int main()
    {
        scanf("%d%d", &n, &m);
        init();
        for(int i = 1; i <= m; ++i)
        {
            f[0][i] = 1;
            for(int j = 1; j <= i; ++j)
                f[j][i] = (f[j-1][i] + f[j][i-1]) % mod;
        }
        for(int i = 1; i <= n; ++i)
            dp[i][0] = i;
        for(int i = 1; i <= n; ++i)
            for(int j = 1; j <= m; ++j)
                dp[i][j] = (((dp[i-1][j] + C[i+j-1][j] + dp[i][j-1] - C[i+j-1][i] + f[i][j-1]) % mod) + mod) % mod;
        printf("%lld
    ", dp[n][m]);
        return 0;
    }
    View Code

    算法二(数学)

       这个算法的思路很巧妙, 是从这篇博客中学来的

      如果在坐标系中把$1$看作是向右走一步, $-1$看作是向上走一步, 起点是$(0, 0)$,  终点是$(n, m)$, 那么任意一个序列就会变成从$(0, 0)$出发, 只能向右或向上走,走到$(n, m)$的一条路径

      设$f[i]$为最大前缀和大于等于i的序列个数

      那么$f[i]$就等于路径中存在一点$(x, y)$满足如下条件的路径数,$x$,$y$使得$i leqslant x - y$, 即$y leqslant x - i$, $(1 leqslant x leqslant n, 1 leqslant y leqslant m)$

      结合线性规划的思想,也就是说$f[i]$等于路径经过直线$y = x - i$ 及其下面区域的路径数

      考虑任意一条路径都会到$(n, m)$,因此当$1 leqslant i leqslant n - m$ 时, $f[i] = C(n + m , n)$

      而当$max(n - m, 1) leqslant i leqslant n$ 时, 我们需要把路径转化一下,起点不再是$(0, 0)$,而是点$(0,0)$关于直线$y = x - i$的对称点$(i, -i)$,终点不变, 同样是要走$n + m$步走到$(n, m)$,因为只能向右和向上走,那么就一定会经过直线$y = x - i$ 及其下面区域。由于我们是把起点对称到了$(i, -i)$,所以从$(i, -i)$出发经过$y = x - i$与从$(0,0)$出发经过$y = x - i$两种情况的路径是映射的关系,也就是说路径是一一对应的,因此此时$f[i] = C(n + m, n - i)$。

      显然除了上述两种情况外的i, 都有$f[i] = 0$

      那么最终的答案为$sum_{i = 1}^{n} i * (f[i] - f[i+1])$

      预处理阶乘与阶乘逆元,以便快速求出组合数

      时间复杂度为$O(N+M)$

      路径的转化是这种算法的关键, 也是巧妙所在

     代码:

    #include<cstdio>
    #include<cstring>
    #include<iostream>
    #include<algorithm>
    using namespace std;
    typedef long long ll;
    const int maxn = 4004, mod = 998244853;
    
    int n, m;
    ll fac[maxn], inv[maxn], f[maxn], ans = 0;
    
    void init()
    {
        fac[0] = 1LL;
        for(int i = 1; i <= n + m; ++i)
            fac[i] = 1LL * fac[i-1] * i % mod;
        inv[0] = inv[1] = 1LL;
        for(int i = 2; i <= n + m; ++i)
            inv[i] = 1LL * (mod - mod / i) * inv[mod%i] % mod;
        for(int i = 2; i <= n + m; ++i)
            inv[i] = inv[i-1] * inv[i] % mod;
    }
    
    ll comb(int x, int y)
    {
        return (fac[x] * inv[y] % mod) * inv[x-y] % mod;
    }
    
    ll calc(int x)
    {
        if(x <= n - m)
            return comb(n + m, n);
        return comb(n + m, n - x);
    }
    
    int main()
    {
        scanf("%d%d", &n, &m);
        init();
        for(int i = 1; i <= n; ++i)
            f[i] = calc(i);
        for(int i = 1; i <= n; ++i)
            ans = (ans + (1LL * i * (f[i] - f[i+1] + mod) % mod)) % mod;
        printf("%lld
    ", ans);
        return 0;
    }
    View Code
  • 相关阅读:
    【Lintcode】099.Reorder List
    【Lintcode】098.Sort List
    【Lintcode】096.Partition List
    【Lintcode】036.Reverse Linked List II
    C++中使用TCP传文件
    链表中倒数第k个结点
    剪贴板(进程通信)
    调整数组顺序使奇数位于偶数前面
    TCP数据流
    快速幂和同余模
  • 原文地址:https://www.cnblogs.com/Joker-Yza/p/11613901.html
Copyright © 2020-2023  润新知