【训练题32:多重集康托展开 | 数位DP】[HAOI2010]计数 | 洛谷 P2518

洛谷 P2518:计数

难度

省 选 / N O I − \color{purple}省选/NOI- /NOI

− 971 2.28 k -\frac{971}{2.28k} 2.28k971

题意 + 数据范围 + 样例解释

  • 给你一个 0 ∼ 9 0\sim9 09数字的多重集排列,比如 1020 1020 1020
    问你该排列是该多重集全排列的字典序第几排列 ( b a s e   0 ) (base\ 0) (base 0)

    多重集合大小 ≤ 50 \le 50 50
    答案保证不会超过 2 63 − 1 2^{63}-1 2631,所以答案不取模。
  • 对于多重集合 { 0 , 0 , 1 , 2 } \{0,0,1,2\} {0,0,1,2},按字典序的全排列为:
    0012 , 0021 , 0102 , 0120 , 0201 , 0210 , 1002 , 1020 , 1200 , 2001 , 2010 , 2100 0012,0021,0102,0120,0201,0210,1002,1020,1200,2001,2010,2100 0012,0021,0102,0120,0201,0210,1002,1020,1200,2001,2010,2100
    所以 1020 1020 1020 是第 7 7 7 个排列。( 0012 0012 0012 是第 0 0 0 个排列)

康托展开

公式:

  • 字典序号 = ∑ n i = 1 S i × ( n − i ) ! =\underset{i=1}{\overset{n}{\sum}}S_i\times (n-i)! =i=1nSi×(ni)!
    其中 S i S_i Si 表示目前还没有用过的所有数里面,小于给定排列第 i i i 个数的个数。
  • 公式什么鬼?初学肯定一脸蒙蔽,但是推导过程其实很简单:
    考虑全排列 3 , 4 , 1 , 2 3,4,1,2 3,4,1,2。这里 3 3 3 是排列第一位, 1 1 1 是第四位。
  1. 你现在要构造一个新的全排列,你希望算出所有比原排列小的全排列个数。
  2. 如果你第一位选择了 1 1 1 或者 2 2 2 ,那么你后面三个数无论怎么排列,构造出的全排列一定比给的排列要字典序更小。个数 = 2 × ( 4 − 1 ) ! =2\times (4-1)! =2×(41)!
  3. 然后你第一位只能选择 3 3 3 了,如果选择 4 4 4 是不可能构造出比原排列小的全排列的。
  4. 接下来第二位,要想后面随便排列,你这一位能选 1 1 1 2 2 2,因为数字3已经被你选过了。个数 = 2 × ( 4 − 2 ) ! =2\times (4-2)! =2×(42)!
  5. 接下来第三位,没有比 1 1 1 还小的数,那就个数 = 0 × ( 4 − 3 ) ! =0\times(4-3)! =0×(43)!,这一位只能选 1 1 1
  6. 最后一位,没有比 2 2 2 还小的没有选过的数。个数 = 0 × ( 4 − 4 ) ! =0\times(4-4)! =0×(44)!
  7. 最后总的答案等于上面所有个数相加 = 16 =16 =16,即该全排列是第 16 16 16 个全排列。

优化:

  • 如果你直接找有多少个数是目前没有用过的,那时间复杂度 O ( N 2 ) O(N^2) O(N2)
    我们可以用树状数组或者线段树记录 [ 0 , x ] [0,x] [0,x] 中还有多少个数没有用。
    q u e r y ( 0 , x ) query(0,x) query(0,x) 表示 [ 0 , x ] [0,x] [0,x] 范围内没有用过的数的个数,则
    字典序号 = ∑ n i = 1 q u e r y ( 0 , a [ i ] − 1 ) × ( n − i ) ! =\underset{i=1}{\overset{n}{\sum}}query(0,a[i]-1)\times (n-i)! =i=1nquery(0,a[i]1)×(ni)!
    时间复杂度 O ( N log ⁡ N ) O(N\log N) O(NlogN)

核心代码:

/*
 _            __   __          _          _
| |           \ \ / /         | |        (_)
| |__  _   _   \ V /__ _ _ __ | |     ___ _
| '_ \| | | |   \ // _` | '_ \| |    / _ \ |
| |_) | |_| |   | | (_| | | | | |___|  __/ |
|_.__/ \__, |   \_/\__,_|_| |_\_____/\___|_|
        __/ |
       |___/
*/
const int MAX = 1e6+50;
const ll  MOD = 998244353;

ll fac[MAX];
ll aa[MAX];
ll cc[MAX];
int n;

ll lowbit(ll x){
    return x & (-x);
}
ll upd(ll x,ll c){
    while(x <= n){
        cc[x] += c;
        x += lowbit(x);
    }
}
ll query(ll x){
    ll res = 0;
    while(x){
        res += cc[x];
        x -= lowbit(x);
    }
    return res;
}
void init(int x){
    fac[0] = 1;
    for(int i = 1;i <= x;++i){
        fac[i] = fac[i-1] * i % MOD;
    }
}
int main()
{
    scanf("%d",&n);
    for(int i = 1;i <= n;++i){
        scanf("%d",&aa[i]);
        upd(i,1);
    }
    init(n);
    ll ans = 1;
    for(int i = 1;i <= n;++i){
        ans = (ans + query(aa[i] - 1) * fac[n - i] % MOD) % MOD;
        upd(aa[i],-1);
    }
    cout << ans;
    return 0;
}

有限多重集的全排列

  • 想要知道这题的解法,这个知识需要掌握。
  • 假设有 n n n 种不同元素,第 i i i 种元素的个数为 c n t i cnt_i cnti
    则该有限多重集的全排列方案数为:
    a n s = ( ∑ n i = 1 c n t i ) ! ∏ n i = 1 ( c n t i ! ) ans=\frac{(\underset{i=1}{\overset{n}{\sum}}cnt_i)!}{\underset{i=1}{\overset{n}{\prod}}(cnt_i!)} ans=i=1n(cnti!)(i=1ncnti)!
  • 但是这题不能取模,这么算明显会爆 L L LL LL,我们可以使用朴素做法:
    一共 s h u = ∑ c n t i shu=\sum cnt_i shu=cnti 种元素,一开始选择 c n t 1 cnt_1 cnt1 个位置填充一号元素,方案数 C s h u c n t 1 C_{shu}^{cnt_1} Cshucnt1
    然后在余下的 s h u − c n t 1 shu-cnt_1 shucnt1个位置选择 c n t 2 cnt_2 cnt2 个位置填充二号元素,方案数 C s h u − c n t 1 c n t 2 C_{shu-cnt_1}^{cnt_2} Cshucnt1cnt2
    ⋮ \qquad\qquad\qquad\vdots
    最后在 s h u − c n t 1 − ⋯ − c n t n − 1 shu-cnt_1-\cdots-cnt_{n-1} shucnt1cntn1 个位置选择 c n t n cnt_n cntn 个位置填充 n n n 号元素
    则该有限多重集的全排列方案数为:
    a n s = C s h u c n t 1 × C s h u − c n t 1 c n t 2 × ⋯ × C s h u − c n t 1 − ⋯ − c n t n − 1 c n t n ans=C_{shu}^{cnt_1}\times C_{shu-cnt_1}^{cnt_2}\times\cdots\times C_{shu-cnt_1-\cdots-cnt_{n-1}}^{cnt_n} ans=Cshucnt1×Cshucnt1cnt2××Cshucnt1cntn1cntn

多重集康托展开

  • 其实思路和原来一样,写成公式比较麻烦,但是写成算法会比较简单。
  • 对于第 i i i 位,如果我们放置字典序小于原排列的元素,那么后面的所有元素的排列数就是多重集的全排列
    接下来,我们第 i i i 位放置和原排列相同的元素,再递推到 i + 1 i+1 i+1位,重复这个步骤。
  • 然后就……没了?确实没了。

核心代码

时间复杂度: O ( 1 0 2 × n ) O(10^2\times n) O(102×n) 10 10 10是因为这里数字只有 0 ∼ 9 0\sim 9 09

/*
 _            __   __          _          _
| |           \ \ / /         | |        (_)
| |__  _   _   \ V /__ _ _ __ | |     ___ _
| '_ \| | | |   \ // _` | '_ \| |    / _ \ |
| |_) | |_| |   | | (_| | | | | |___|  __/ |
|_.__/ \__, |   \_/\__,_|_| |_\_____/\___|_|
        __/ |
       |___/
*/
#define ll long long
const int MAX = 50+50;

ll CC[MAX][MAX];
string ss;
int n;

ll C(int n,int m){		/// 记忆化组合数
    /// Cnm=Cn-1m+Cn-1m-1
    if(CC[n][m])return CC[n][m];
    if(n < m)return 0;
    if(m == 1)return n;
    if(m == 0 || n == m)return 1;
    CC[n][m] = C(n-1,m) + C(n-1,m-1);
    return CC[n][m];
}

int cnt[15];			/// 记录每个数字的出现次数
ll solve(){				/// 求有限多重集全排列个数。
    int shu = 0;
    for(int i = 0;i < 10;++i)shu += cnt[i];

    ll res = 1LL;
    for(int i = 0;i < 10;++i){
        if(cnt[i]){
            res = res * C(shu,cnt[i]);
            shu -= cnt[i];
        }
    }
    return res;
}

int main()
{
    cin >> ss;
    n = ss.size();

    for(int i = 0;i < n;++i){
        cnt[ss[i]-'0']++;
    }
    ll ans = 0;
    for(int i = 0;i < n;++i){
        for(char j = '0';j < ss[i];++j){
            if(cnt[j-'0']){				/// 放一个比原排列该位置字典序小的元素
                cnt[j-'0']--;
                ans = ans + solve();
                cnt[j-'0']++;
            }
        }
        cnt[ss[i]-'0']--;				/// 放一个原排列该位置的元素
    }
    cout << ans;
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值