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

题目传送门: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这个位置,那么就有下图:
数论|Codeforces Round #445 Div1 C(Maximum Element (组合数学+DP))
文章图片

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

f[i]=∑x=i?ki?1f[i?x?1]?Ci?x?1i?2?(x?1)!
然而这个是不可以再优化的。于是我们不妨记 g[i]=f[i]i! ,这样 f[i?x?1]?Ci?x?1i?2?(x?1)! 就变成了 g[i?x?1]?(i?2)! 。然后对g做个部分和即可。
最终输出答案的时候,可以枚举一下n放在了哪个位置(假设在第i位),那么方案数就是 f[i]?(n?1)! 。然后用总方案数减去合法的方案数,即可算出不合法的方案数。
预处理阶乘和线性逆元,时间就是 O(n) 的。
【数论|Codeforces Round #445 Div1 C(Maximum Element (组合数学+DP))】CODE:

#include #include #include #include #include #include #include #include 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; }

    推荐阅读