51nod 1843 排列合并机

题目描述:

小C有一个合并排列机,它可以合并两个长度为n的排列A,B。

合并一共有2*n步,一开始答案的数列为空,每一步有两个选项:

(1).若A不为空,则你可以删掉A开头的元素,放到答案数列的末尾。

(2).若B不为空,则你可以删掉B开头的元素,放到答案数列的末尾。

以上两个选项每一步只能选一个

小C不喜欢重复的东西,定义两个排列的价值f(A,B)为他们合并可以生成的答案数列的种数

两个答案数列不同当且仅当某一位不同

现在小C想知道,随机选择两个长度为n的排列A,B。求f(A,B)的期望

为了避免精度误差,你需要输出ans*(n!)*(n!)对p取模的值,显然这是个整数

 收起

输入

第一行两个正整数n,p
1<=n<=100
10^4<=p<2^30
保证p是个质数

输出

输出一个非负整数表示答案

输入样例

2 23333

输出样例

12

题解:

博弈论+dp

Nim z utrudnieniem

AA和BB在进行NimNim博弈,AA是先手,但是BB在开始之前可以扔掉dd的倍数堆的石子(不能扔掉全部的),问BB有多少种扔的办法使BB必胜。

显然BB必胜的条件为a1⊕a2⊕⋯an=0a1⊕a2⊕⋯an=0(NIMNIM博弈的结论)

显然可以这么来定义状态f[i][j][k]f[i][j][k]表示前ii堆石子,扔掉了jj堆(在mod dmod d下),异或和是kk的方案数

f[i][j][k]=f[i−1][j][k]+f[i−1][j−1][k⊕ai]f[i][j][k]=f[i−1][j][k]+f[i−1][j−1][k⊕ai]

复杂度为O(n⋅d⋅maxai)O(n⋅d⋅maxai)

现在的问题在于怎么把maxaimaxai这一项变小?

有一个神奇的结论

因为对于任意一个正整数NN,它和比它自己小的数字异或起来的结果不会超过N×2N×2

所以只需要把aiai从小到大排序,这样复杂度就变成O(m⋅d)O(m⋅d)

现在问题在于空间开销太大了。

显然可以用滚动数组,但是仍然太大,因为我们必须要把ii这一维给省掉。

只需要在每一层ii时,按照kk从大到小的顺序进行转移即可。

还要注意nn是dd的倍数时,会算上全选的情况,答案需要减去1

Code:

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 105;
int n, mod, g[MAXN], f[MAXN][MAXN][MAXN], fac[MAXN<<1], inv[MAXN<<1];
int C(int n, int m) { return m > n ? 0 : 1ll * fac[n] * inv[m] % mod * inv[n-m] % mod; }
int main(){
    scanf("%d%d", &n, &mod);
    fac[0] = inv[0] = inv[1] = fac[1] = 1;
    for(int i = 2; i <= (n<<1); ++i){
        fac[i] = 1ll * fac[i-1] * i %mod;
        inv[i] = 1ll * (mod - mod/i) * inv[mod%i] % mod;
    }
	for(int i = 2; i <= (n<<1); ++i) inv[i] = 1ll * inv[i-1] * inv[i] % mod;
    for(int i = 1; i <= n; ++i) g[i] = C(2*i - 2, i - 1) - C(2*i - 2, i - 2);
    f[0][0][0] = 1;
    for(int i = 0; i <= n; ++i)
        for(int j = 0; j <= n; ++j) if(i || j) {
            for(int k = max(0, i+j-n); k <= i && k <= j; ++k){
                int &ret = f[i][j][k];
                if(i && k) ret = (ret + 1ll * f[i-1][j][k-1] * (j-k+1)) % mod;
                if(j && k) ret = (ret + 1ll * f[i][j-1][k-1] * (i-k+1)) % mod;
                if(i) ret = (ret + 1ll * f[i-1][j][k] * (n - (i-1+j-k))) % mod;
                if(j) ret = (ret + 1ll * f[i][j-1][k] * (n - (i+j-1-k))) % mod;
                for(int d = 1; d <= k; ++d)
                    ret = (ret - 1ll * f[i-d][j-d][k-d] * C(n-(i+j-k-d), d) % mod * g[d] % mod * fac[d] % mod) % mod;
            }
        }
    printf("%d\n", (f[n][n][n] + mod) % mod);
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值