A Ribbon for Tomorrow

A Ribbon for Tomorrow

(快速幂,组合数,逆元)

1. 题目简述

给定一个长度为 \(n\) 的二进制字符串 \(s\)(由 0 和 1 组成)。允许进行任意次如下操作:

  • 选择两个下标 \(l \le r\),若当前字符串中 \(s_l = s_r\),则反转子串 \(s_l s_{l+1} \dots s_r\)。

问:从初始字符串出发,可以得到的不同的二进制字符串数量是多少?答案对 \(998244353\) 取模。


2. 核心观察:操作的不变量

通过分析操作的性质,我们可以找到三个在操作前后保持不变的量:

2.1 首字符不变

  • 若操作包含第一位(即 \(l=1\)),反转后新的首字符是原来的 \(s_r\)。由于必须满足 \(s_l = s_r\),且 \(s_l = s_1\),所以新首字符仍等于原首字符。
  • 若操作不包含第一位,首字符完全不受影响。
  • 结论:字符串的 第 1 位字符 永远固定不变。

2.2 字符总数不变

  • 反转操作只改变字符顺序,不改变字符本身。
  • 结论:字符 0 的总数 \(cnt_0\) 和字符 1 的总数 \(cnt_1\) 保持不变。

2.3 连续相同字符段(Run)的数量不变

定义:Run(连续段)是指字符串中极长的连续相同字符子串。例如 000 是一个 0-run,11 是一个 1-run。

  • 反转一个两端字符相同的子串时,子串内部的顺序倒置,但子串两端字符相同,因此与左右邻接部分的连接处不会产生新的 Run,也不会导致现有 Run 消失。
  • 结论:0-run 的数量 \(seg_0\) 和 1-run 的数量 \(seg_1\) 保持不变。

推论:由于 Run 的颜色一定是交替出现的,加上首字符固定,Run 的数量就决定了整个字符串的 Run 顺序框架。例如,若首字符为 0,且有 2 个 0-run 和 1 个 1-run,则 Run 顺序只能是:0-run, 1-run, 0-run。


3. 充分性证明:这些不变量是否足够?

题解的核心在于证明:任何满足上述不变量的状态都是可达的。

3.1 关键操作:交换同色 Run 的长度

考虑字符串中存在以下结构(两个 0-run 夹着一个 1-run):

\[0^a \; 1^b \; 0^c \]

其中 \(a, b, c > 0\),上标表示该字符重复的次数。

我们选择左端点位于第一个 0-run,右端点位于第二个 0-run,反转整个子串。由于两端都是 0,反转后变为:

\[0^c \; 1^b \; 0^a \]

中间的 \(1^b\) 反向后不变。

这意味着:我们可以在不改变中间 1-run 长度的情况下,任意交换两个 0-run 的长度。

3.2 推广到所有 Run

  • 因为任意两个同色的 Run 之间一定隔着若干个异色 Run,我们可以利用上述操作,将长度在同色 Run 之间任意调换。
  • 这相当于:你有 \(seg_0\) 个盒子,里面装着总数为 \(cnt_0\) 个 0,允许你任意交换两个盒子里的数量。显然,只要每个盒子最终非空,你可以达到任意一种分配方案。
  • 同理,1 的分配也是独立的,且不会干扰 0 的分配。

结论:只要保证每个 Run 长度为正整数,且总数固定,所有长度组合都是可达的。


#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll MOD = 998244353;
const int MAXN = 1000000 + 5;
vector<ll> fact(MAXN), invfact(MAXN);

ll modpow(ll a, ll e) {
    ll r = 1;
    while(e) {
        if(e & 1) r = r * a % MOD;
        a = a * a % MOD;
        e >>= 1;
    }
    return r;
}
void init() {
    fact[0] = 1;
    for(int i = 1; i < MAXN; i ++) fact[i] = fact[i - 1] * i % MOD;
    invfact[MAXN-1] = modpow(fact[MAXN - 1], MOD - 2);
    for(int i = MAXN - 2; i >= 0; i --) invfact[i] = invfact[i + 1] * (i + 1) % MOD;
}
ll C(int n, int k) {
    if(k < 0 || k > n) return 0;
    return fact[n] * invfact[k] % MOD * invfact[n-k] % MOD;
}

void solve() {
    int n;
    string s; cin >> n >> s;
    int cnt0 = 0, cnt1 = 0;
    int seg0 = 0, seg1 = 0;

    for(char ch : s) {
        if(ch == '0') cnt0 ++;
        else cnt1 ++;
    }

    seg0 = seg1 = 0;
    for(int i = 0; i < n; ) {
        int j = i;
        while (j < n && s[j] == s[i]) j ++;
        if (s[i] == '0') seg0 ++;
        else seg1 ++;
        i = j;
    }
    
    ll ways0 = 1, ways1 = 1;
    if(cnt0 > 0) ways0 = C(cnt0 - 1, seg0 - 1);
    if(cnt1 > 0) ways1 = C(cnt1 - 1, seg1 - 1);

    ll ans = ways0 * ways1 % MOD;
    cout << ans << "\n";
}

int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr);
    init();
    int t; cin >> t;
    while(t --) solve();
    return 0;
}
posted @ 2026-09-25 09:27  thenrify  阅读(16)  评论(0)    收藏  举报