Codeforces Round #445 Div1 C:Maximum Element (组合数学+DP)

27 篇文章 0 订阅
23 篇文章 0 订阅

题目传送门:http://codeforces.com/contest/889/problem/C


题目大意:有一个函数,传一个长度为n的数组a进去,并设计一个参数k,它会用以下方式帮你找最大值:从左往右扫a,并记录当前最大值。如果变更完当前最大值之后,再扫连续的k个元素,当前最大值都没有变(或者扫完了a),它就会直接返回当前最大值。现在给定n,k,你要输出有多少个1~n的排列a,会使得这个函数不返回n。 n,k<=106


题目分析:这题想了我一个下午+半个晚上。当我最终灵光一现想到解法,并把那短短的几行代码码完AC的时候,我想起了一句诗:“两句三年得,一吟双泪流”。(也许我的DP还是太菜了)

这题很明显是DP,但主要问题是怎么记状态。如果我们记长度为一维,最大值放在哪里(或者统计答案的时候能扫到a数组的哪里)为另一维,显然已经超时。对此,我们需要对状态做一些约束,最后用组合数学来帮忙计算答案。

设f[i]表示1~i的排列中,i放在最后一位,且最终返回值为i的方案数。很明显,i-1放的位置应该是i-k~i-1。假设我们已经枚举了i-1放在i-x这个位置,那么就有下图:

其中绿色部分是一个子问题,红色部分可以自由组合。由于{2 5 6 8}和{1 2 3 4}是等价的,所以左边的方案数为 f[ix1]Cix1i2 。而左边重编号之后,右边可以自由组合,所以右边的贡献是 (x1)! 。这样就有:

f[i]=x=iki1f[ix1]Cix1i2(x1)!

然而这个是不可以再优化的。于是我们不妨记 g[i]=f[i]i! ,这样 f[ix1]Cix1i2(x1)! 就变成了 g[ix1](i2)! 。然后对g做个部分和即可。

最终输出答案的时候,可以枚举一下n放在了哪个位置(假设在第i位),那么方案数就是 f[i](n1)! 。然后用总方案数减去合法的方案数,即可算出不合法的方案数。

预处理阶乘和线性逆元,时间就是 O(n) 的。


CODE:

#include<iostream>
#include<string>
#include<cstring>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<stdio.h>
#include<algorithm>
using namespace std;

const int maxn=1000100;
const long long M=1000000007;
typedef long long LL;

LL f[maxn];
LL fac[maxn];
LL rev[maxn];

int n,k;
LL ans=0;

int main()
{
    freopen("2329.in","r",stdin);
    freopen("2329.out","w",stdout);

    scanf("%d%d",&n,&k);
    fac[0]=1;
    for (int i=1; i<=n; i++) fac[i]=fac[i-1]*(long long)i%M;
    rev[0]=rev[1]=1;
    for (int i=2; i<=n; i++)
    {
        LL x=M/i,y=M%i;
        rev[i]=x*x%M*rev[y]%M*rev[y]%M*(long long)i%M;
    }
    for (int i=2; i<=n; i++) rev[i]=rev[i]*rev[i-1]%M;

    f[1]=1;
    LL sum=1;
    for (int i=2; i<=n; i++)
    {
        f[i]=sum*fac[i-2]%M;
        f[i]=f[i]*rev[i-1]%M;
        sum=(sum+f[i])%M;
        if (i-k>=1) sum=(sum-f[i-k]+M)%M;
    }
    for (int i=1; i<=n; i++) ans=(ans+f[i]*fac[n-1]%M)%M;
    ans=(fac[n]-ans+M)%M;
    printf("%lld\n",ans);

    return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值