Loading

Mercenaries

思路

今天时间剩的不多, 还是看看得了
发现听过某个巨佬讲这道题, 可惜忘了

你发现约束条件数 \(m\) 很小啊, 容易想到状压, 但这是后事了
先考虑一下有没有什么符合直觉的做法, 你发现他求 \(n\) 个元素的子集? 这我写鸡毛啊
算了反正状态不好, 复习一下回寝了
下一次写这个题还要等到考完试补完题, 很蓝的啦


考完复活了

简化题意

\(n\) 个人和 \(m\) 对敌对关系, 每一个人有一个条件区间 \([l_i,r_i]\)
现在要在这 \(n\) 个人中选 若干个人 , 定义一个合法的选择 \(\{S\}\) , 当且仅当对于 \(\{S\}\) 中的所有人 \(i\) , \(l_i\le \lvert S \rvert \le r_i\), 且 \(\{S\}\) 中所有人互不敌对
求有多少种合法的选择

因为求 \(n \leq 3 \times 10^5\) 这个的所有子集显然不可能, 考虑有没有什么好的性质可以避开
先考虑互不敌对这个条件, 你发现因为敌对数很少啊, 所以互不敌对的子集其实是很多的, 我们考虑正难则反, 考虑敌对子集
这个是简单的, 我们可以记录满足了哪些敌对状态, 仅仅只有 \(2^{20} - 1\) 个状态
知道了满足了那些敌对状态, 那么就知道必须要有哪些人出现, 然后其他人任选

好那么进入正题, 你发现给定「满足敌对状态」, 我们只能「钦定」这些人出现, 也就是说我们只能找到「至少」出现了 \(k\) 个敌对状态的方案数, 套路的, 记为 \(f(k)\)

然后你套路的令 \(g(k)\) 为「恰好」出现了 \(k\) 个敌对状态的方案数, 然后就计算出来了


需要证明的是, 二项式反演能否在这里使用

我们都知道这个柿子

\[f(k) = \sum_{i = k}^{n} {i \choose k} g(i) \iff g(k) = \sum_{i = k}^{n} (-1)^{i - k} {i \choose k} f(i) \]

容易发现的是在这种情况下, \(f(k)\) 的表达式仍然正确, 所以仍然可以这样反演

那么

\[ans = g(0) = \sum_{i = 0}^{n} (-1)^i f(i) \]

说个大家不爱听的, 容斥真王朝了


但是你发现什么呢, 我们之前没有考虑 条件区间 这个东西, 但是这个肯定是要加入讨论的, 也就是说, 求 \(f(k)\) 的方法还需要讨论

首先确定 \(f(k)\) 的意义是什么 : \(f(k)\) 表示「钦定」\(k\) 个敌对关系必须成立的满足条件区间的子集方案数
容易发现的是, 对于敌对关系的状态 \(\mathbb{S}\) , 我们可以「钦定」出那些人必须在选择集合之中, 如果这些人之间不满足条件区间, 那么一定不成立, 否则考虑向外填其他不在敌对关系之中的人, 但是如何确定满足条件区间的要求?

首先我们记敌对关系产生的子集为 \(\mathbb{U}\) , 那么先找到 \(\mathbb{U}\)
\(\displaystyle L = \max_{i \in \mathbb{U}} l_i\)\(\displaystyle R = \min_{i \in \mathbb{U}} r_i\) , 最终的子集大小一定要在 \([L, R]\) 之间
我们考虑当前的约束条件有那些 (假设加入的人的点集为 \(\mathbb{P}\) , 其中 \(\mathbb{P} \cap \mathbb{U} = \varnothing\)):

  • \(L \leq \lvert \mathbb{U} \rvert + \lvert \mathbb{P} \rvert \leq R\)
  • $\forall i \in \mathbb{P}, l_i \leq \lvert \mathbb{U} \rvert + \lvert \mathbb{P} \rvert \leq r_i $

不好处理的是第二个限制, 我们考虑预处理
可以预处理出最终选点个数一定时(即确定 \(\lvert \mathbb{U} \rvert + \lvert \mathbb{P} \rvert\)) , 可能的选点方案数
你容易发现, 满足 \(l_i \leq x \leq r_i\) 的数的个数是一定的, 我们记为 \(t_x\)
这样我们可以计算 \(f(k)\)

\[f(k) = \sum_{|\mathbb{S}| = k \to |\mathbb{U}|} \sum_{i = L}^{R} {t_i - |\mathbb{U}| \choose i - |\mathbb{U}|} \]

实现

为了检验思路的正确性, 还是要自己写代码

框架

首先是 \(t\) 数组的计算用差分处理, 然后就是组合数部分要预处理不然会 \(\rm{TLE}\)

代码

#include <bits/stdc++.h>
#define int long long
const int MAXM = 50;
const int MAXVAL = 3e5 + 20;
const int MOD = 998244353;

inline int read() {
    int x = 0, f = 1;
    char c = getchar();
    while (!isdigit(c)) {
        if (c == '-') f = -1;
        c = getchar();
    }
    while (isdigit(c)) x = x * 10 + c - '0', c = getchar();
    return x * f;
}
int n, m;

int cf[MAXVAL], l[MAXVAL], r[MAXVAL], cnt[MAXVAL], a[MAXM], b[MAXM];
int fac[MAXVAL], inv[MAXVAL];

int qpow(int x, int e) {
    int ans = 1;
    while (e) {
        if (e & 1) ans = ans * x % MOD;
        x = x * x % MOD; e >>= 1;
    }
    return ans;
}

void init() {
    fac[0] = 1;
    for (int i = 1; i <= 300000; i++) fac[i] = fac[i - 1] * i % MOD;
    for (int i = 0; i <= 300000; i++) inv[i] = qpow(fac[i], MOD - 2);
}

int sum[MAXVAL][MAXM];
inline int C(int x, int y) {
    if (x < y || x < 0 || y < 0) return 0;
    return fac[x] * inv[y] % MOD * inv[x - y] % MOD;
}

signed main()
{
    n = read(); m = read();
    init();

    /*读入 + 差分处理*/
    for (int i = 1; i <= n; i++) {
        l[i] = read(), r[i] = read();
        cf[l[i]]++; cf[r[i] + 1]--;
    }

    int num = 0;
    for (int i = 1; i <= n; i++) {
        num += cf[i]; cnt[i] = num; }
    
    for (int i = 1; i <= m; i++) a[i] = read(), b[i] = read();

    /*预处理系数*/
    for (int i = 0; i <= 40; i++) for (int j = 1; j <= n; j++) sum[j][i] = (sum[j - 1][i] + C(cnt[j] - i, j - i)) % MOD;
    
    int ans = 0;
    for (int S = 0; S < (1 << m); S++) {
        int L = 1, R = n;
        std::set<int> st;
        for (int j = 1; j <= m; j++)
            if (S >> (j - 1) & 1) {
                st.insert(a[j]), L = std::max(L, l[a[j]]), R = std::min(R, r[a[j]]);
                st.insert(b[j]), L = std::max(L, l[b[j]]), R = std::min(R, r[b[j]]);
            }
        if (L > R) continue;

        int calc = (sum[R][st.size()] - sum[L - 1][st.size()] + MOD) % MOD;
        int cnt1 = __builtin_popcount(S);
        if (cnt1 & 1) ans = (ans - calc + MOD) % MOD;
        else ans = (ans + calc) % MOD;
    }
    printf("%lld\n", ans);
    return 0;
}

总结

正难则反, 转化成「钦定」类问题
「钦定」类问题往往要向「二项式反演」转化

约束条件不好处理的时候, 全部列出来冷静分析
预处理可以方便的解决一些问题
直接用组合数不好解决的问题, 你考虑枚举一些信息方便组合数的计算, 常见的是枚举最终选了多少个

posted @ 2025-01-04 11:19  Yorg  阅读(11)  评论(0)    收藏  举报