1 条题解

  • 0
    @ 2025-8-24 23:08:51

    自动搬运

    查看原文

    来自洛谷,原作者为

    avatar ztd___
    时代的眼泪。

    搬运于2025-08-24 23:08:51,当前版本为作者最后更新于2025-01-26 13:48:20,作者可能在搬运后再次修改,您可在原文处查看最新版

    自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多

    以下是正文


    你点开了题目。

    题目中的“方案数对 998244353998244353 取模的结果”,都无时无刻不在告诉你:这是道计数题。

    但是你会不了一点计数。你只学过组合数和 DP,对其他内容一无所知。

    你伤心透顶,却看见榜上过题的人数正在飞快增长,大家用行动提醒你:这题并不难。

    看着 10710^7 的数据范围,你认为这题正解一定是线性,而目前自己学过的线性计数算法只有 DP 了。你用你敏锐的目光发现了 nnmm 两个字母。

    “有 nnmm,那大概率是 DP 了。”但是你忘了,组合数的标准形式就是 CnmC_n^m

    思考正解无果,你决定思考部分分。思考部分分无果后, 你回想起之前自己做过的一些 DP 题目和有关套路,突发奇想,想到了该如何设计状态。

    “部分分做法可以先不用在意复杂度吧,那完全可以用 dpi,jdp_{i,j} 表示前 ii 个数字分成 jj 段的方案数啊!然后当 aiai1a_i \le a_{i-1} 时,必须单独分段,即 dpi,j=dpi1,j1dp_{i,j} = dp_{i-1,j-1},否则可以多分一段,即 dpi,j=dpi1,j+dpi1,j1dp_{i,j} = dp_{i-1,j}+dp_{i-1,j-1},最后注意一下边界不就好了?”

    你兴高采烈,写完代码提交上去,却只有可怜的 30pts。你汗流浃背了。你分析复杂度,发现是 O(nm)O(nm)。那肯定过不了了啊。

    你非常慌张,一直思考怎么将问题化为一维。思考无果。你以 30pts 的好成绩退下了比赛。

    赛后,你观看了题解。

    “哎哎哎不是,这怎么是组合数啊???”

    你悲痛万分,为自己在 DP 上花费的 2h+ 而心如刀绞。

    痛定思痛。你决定仔细研究一下这题。

    你发现这题转化后就是典型的组合数。

    将问题转化为从若干个间隔里选若干个间隔的方案,然后再去掉必须隔开的 aiai1a_i \le a_{i-1} 的情况(设其个数为 cntcnt),总间隔数就是 n1cntn - 1 - cnt,可选择的间隔数可以从 00m1cntm - 1 - cnt 枚举,将答案相加即可。cntcnt 完全可以线性处理。

    形式化地,就是求出:

    i=0m1cntCn1cnti\sum_{i=0}^{m-1-cnt} C_{n-1-cnt}^{i}

    发现这题确实这么简单,你心中更难受了。

    你决定先把框架写了。

    void _solve() {
    	cin >> n >> m;
    	for (int i = 1; i <= n; i++) cin >> a[i];
    	int cnt = 0, ans = 0;
    	for (int i = 2; i <= n; i++) {
    		if (a[i] <= a[i - 1]) cnt++;
    	}
    	for (int i = 0; i <= m - cnt - 1; i++) {
    		ans = (ans + C(n - 1 - cnt, i)) % MOD;
    	}
    	if (cnt > m - 1) cout << 0 << "\n";
    	else cout << ans << "\n";
    }
    

    正当你准备写组合数部分时,你突然发现组合数没法算,用杨辉三角递推太慢了,只能用阶乘直接算。即:

    Cnm=n!(nm)!×m!C_n^m = \frac{n!}{(n-m)! \times m!}

    阶乘可以线性预处理,但除法呢?众所周知,在模意义下不能直接算除法啊。

    你想到了一个东西:乘法逆元

    仔细学习了若干小时后,你学会了线性递推求乘法逆元!

    你开始写代码,然后又发现了问题:阶乘取模后可能还是很大,线性递推推不到那么大。

    你仔细地推了一段公式,其中 inviinv_i 表示 ii 在模 998244353998244353 意义下的逆元:

    i!=(i1)!×i\because i! = (i - 1)! \times i

    invi!=inv(i1)!×i\therefore inv_{i!} = inv_{(i-1)! \times i}

    根据同余易得:$inv_{(i-1)! \times i} = inv_{(i-1)!} \times inv_{i}$

    所以 invi!=inv(i1)!×inviinv_{i!} = inv_{(i-1)!} \times inv_i

    感觉推了跟没推一样啊!但是你发现这个结论确实不需要很难的推理,而且 1!=11! = 1 没有变化,所以可以直接从 11 开始推。你决定开始写代码。

    其中,你为了节省空间,决定用预处理过的 invinv 数组直接处理 i!i! 的逆元。阶乘数组求组合数时需要用来表示分子 n!n!,但是逆元数组没有用处,所以可以直接在逆元数组 invinv 上推。

    你写出了预处理代码。

    const int N = 1e7 + 77;
    const int MOD = 998244353;
    int inv[N], fac[N], n, m, a[N];
    void _init() {
    	inv[0] = inv[1] = fac[0] = fac[1] = 1;
    	for (int i = 2; i <= 1e7; i++) {
    		inv[i] = (MOD - MOD / i) * inv[MOD % i] % MOD;
    		fac[i] = fac[i - 1] * i % MOD;
    	}
    	for (int i = 2; i <= 1e7; i++) {
    		inv[i] = inv[i - 1] * inv[i] % MOD;
    	}
    }
    

    然后你发现组合数的计算就一行,算上框架也才三行,所以把它写了出来。

    int C(int n, int m) {
    	return fac[n] * inv[n - m] % MOD * inv[m] % MOD;
    }
    

    然后你发现将上述代码块拼起来,加个 main 函数和头文件啥的就 AC 了。

    signed main() {
    	ios::sync_with_stdio(false);
    	cin.tie(0), cout.tie(0);
    	_init();
    	int T;
    	cin >> T;
    	while (T--) _solve();
    	return 0;
    }
    

    你发现自己还是太菜了。

    • 1

    信息

    ID
    9290
    时间
    1200ms
    内存
    512MiB
    难度
    4
    标签
    递交数
    0
    已通过
    0
    上传者