题解:洛谷 P14975 COW Splits

【题目来源】

洛谷:P14975 [USACO26JAN1] COW Splits B - 洛谷

【题目描述】

给定 Bessie 一个正整数 \(N\) 和一个长度为 \(3N\) 的字符串 \(S\),该字符串是由 \(N\) 个长度为 \(3\) 的字符串拼接而成,每个字符串都是 \(\texttt{COW}\) 的一个循环移位。换句话说,每个字符串将是 \(\texttt{COW}\)\(\texttt{OWC}\)\(\texttt{WCO}\) 中的一个。

字符串 \(X\) 是一个平方串,当且仅当存在一个字符串 \(Y\) 使得 \(X = Y + Y\),其中 \(+\) 表示字符串拼接。例如,\(\texttt{COWCOW}\)\(\texttt{CC}\) 是平方串的例子,但 \(\texttt{COWO}\)\(\texttt{OC}\) 不是。

在一次操作中,Bessie 可以从 \(S\) 中移除任意一个子序列 \(T\),其中 \(T\) 是一个平方串。一个字符串的子序列是指通过从原字符串中删除若干(可以是零个)字符而得到的字符串。

你的任务是帮助 Bessie 判断是否可能将 \(S\) 转化为空字符串。此外,如果可能,你必须提供一种实现方法。

Bessie 还会收到一个参数 \(k\),其值为 \(0\)\(1\)。设 \(M\) 为你构造方案中的操作次数。

  • 如果 \(k = 0\),则 \(M\) 必须等于可能的最小操作次数。
  • 如果 \(k = 1\),则 \(M\) 最多可以比可能的最小操作次数多 \(1\)

【输入】

第一行包含 \(T\),表示独立测试用例的数量(\(1\le T\le 10^4\)),以及 \(k\)\(0 \le k \le 1\))。

每个测试用例的第一行有 \(N\)\(1 \le N \le 10^5\))。

每个测试用例的第二行有 \(S\)

所有测试用例的 \(N\) 之和不超过 \(10^5\)

【输出】

对于每个测试用例,按照以下流程输出一行或两行。如果不可能将 \(S\) 转化为空字符串,则在一行中输出 \(-1\)

否则,在第一行输出 \(M\)——你构造方案中的操作次数。在第二行,输出 \(3N\) 个由空格分隔的整数。第 \(i\) 个整数 \(x\) 表示 \(S\) 的第 \(i\) 个字母是在第 \(x\) 次操作中被删除的(\(1 \le x \le M\))。

【输入样例】

3 1
3
COWOWCWCO
4
WCOCOWWCOCOW
6
COWCOWOWCOWCOWCOWC

【输出样例】

-1
1
1 1 1 1 1 1 1 1 1 1 1 1
3
3 3 2 3 3 2 1 1 1 1 1 1 1 1 1 1 1 1

【核心思想】

  1. 问题分析:给定一个长度为 \(3N\) 的字符串 \(S\),它由 \(N\) 个块组成,每个块都是 "COW" 的循环移位(即 "COW""OWC""WCO")。每次操作可以删除 \(S\) 中的一个平方串子序列(即形如 \(Y+Y\) 的字符串,不要求连续)。目标是判断能否通过若干次操作将 \(S\) 删空,并构造一种操作方案。同时,若 \(k=0\) 要求操作次数最小;若 \(k=1\) 允许比最小次数多 \(1\)

关键洞察:由于每个块都是 "COW" 的循环移位,整个字符串 \(S\) 中每种字母恰好出现 \(N\) 次。要删除一个平方串 \(Y+Y\),本质上是选择两个相同的子序列 \(Y\)\(Y'\) 配对删除。核心结论是:\(N\) 为奇数,则无法完成;若 \(N\) 为偶数,则一定能完成,且最小操作次数为 1(当整个字符串本身就是平方串)或 2(否则)。因为可以通过将 \(S\) 分成前后两半,对每对块进行字符匹配,构造出两次平方串删除操作。

  1. 算法选择

    • 构造 + 贪心匹配:首先判断整个字符串 \(S\) 是否为平方串(即前半部分等于后半部分)。若是,则一次操作即可删空。
    • 否则,构造一个 2 次操作的方案:将 \(S\) 分成前后两半,每半长度均为 \(3N/2\)(需 \(N\) 为偶数)。对每对对应块(每块 3 个字符),将匹配的字符分配到操作 1,不匹配的分配到操作 2,使得操作 1 和操作 2 各自删除的字符序列都是平方串。
  2. 关键步骤(基于代码逻辑):

    • 读入\(T, k\),对每个测试用例读入 \(N\) 和字符串 \(S\)(长度为 \(3N\))。
    • 检查 \(N\) 奇偶:若 \(N\) 为奇数,输出 -1(因为无法将字符串分成两个长度相等的部分来构造两次操作)。
    • 检查一次操作可行性:判断 \(S\) 的前半段是否等于后半段。若是,输出 1 和所有字符的操作编号为 1。
    • 构造两次操作
      • 对于 \(i\) 从 0 到 \(N/2 - 1\),取前半段的第 \(i\) 个块和后半段的第 \(i\) 个块。
      • 比较两个块中的三个字符,按照三种情况(s[l] == s[r]s[l] == s[r+1]s[l] == s[r+2])将匹配的字符分配到操作 1,不匹配的分配到操作 2。
      • 输出 2 和所有字符的操作编号。
  3. 时间/空间复杂度

    • 时间复杂度:\(O(N)\),每个测试用例线性扫描。
    • 空间复杂度:\(O(N)\),存储结果数组。
  4. 构造 + 匹配的核心思想

    • 平方串删除的本质:一次操作删除一个平方串 \(Y+Y\),意味着需要将 \(S\) 中的字符分成两组,每组内的字符序列相同。若要一次删完,则 \(S\) 本身必须是平方串。
    • 两次操作的构造:将 \(S\) 分成两半,每半视为一个“模板”,通过调整字符分配到两个操作,使得每个操作中前半和后半的字符序列相同。由于每个块都是 "COW" 的循环移位,块内三个字符互不相同,通过比较两个块的对应位置,总能找到一种配对方式,将三个字符分配给两个操作,使得每个操作得到的两个子序列相同。
    • 奇偶限制\(N\) 必须为偶数,因为需要将块平均分成两组(前半和后半),否则无法构造。
    • 适用场景:适用于具有固定块结构(如循环移位)的字符串,要求通过删除平方串子序列来清空字符串的构造问题。核心是利用块的置换特性进行贪心匹配。

【解题思路】

【算法标签】

普及+ #构造

【代码详解】

#include <bits/stdc++.h>
using namespace std;

const int N = 100005 * 3;  // 数组最大容量
int t, k, n, num1, num2, num3;  // t: 测试用例数,k: 参数(未使用),n: 字符串长度,其他为临时变量
string s;  // 输入字符串
int a[N];  // 结果数组,存储每个字符的分配方案

// 检查字符串是否可被平分为两个相同的子串
bool check(string t)
{
    for (int i = 0; i < s.size() / 2; i++)
    {
        if (s[i] != s[i + s.size() / 2])
        {
            return false;
        }
    }
    return true;
}

int main()
{
    cin >> t >> k;  // 读入测试用例数和参数k(k未使用)

    while (t--)  // 处理每个测试用例
    {
        cin >> n;  // 读入字符串长度
        cin >> s;  // 读入字符串

        // 如果字符串长度为奇数,无法平分
        if (n % 2 == 1)
        {
            cout << -1 << endl;
            continue;
        }

        // 如果字符串已经可以平分为两个相同的子串
        if (check(s))
        {
            cout << 1 << endl;  // 只需要1种分配方案
            for (int i = 0; i < s.size(); i++)
            {
                cout << 1 << " ";  // 所有字符分配到第1组
            }
            cout << endl;
            continue;
        }

        // 否则需要2种分配方案
        // 将字符串分成前后两半,每3个字符为一组进行匹配
        for (int i = 0; i < n / 2; i++)
        {
            int l = i * 3;      // 前半段当前组的起始位置
            int r = (i + n / 2) * 3;  // 后半段对应组的起始位置

            // 情况1:l位置的字符与r位置的字符相同
            if (s[l] == s[r])
            {
                a[l] = a[l + 1] = a[r] = a[r + 1] = 1;  // 这些位置分配到第1组
                a[l + 2] = a[r + 2] = 2;  // 剩余位置分配到第2组
            }
            // 情况2:l位置的字符与r+1位置的字符相同
            else if (s[l] == s[r + 1])
            {
                a[l] = a[l + 1] = a[r + 1] = a[r + 2] = 1;
                a[l + 2] = a[r] = 2;
            }
            // 情况3:l位置的字符与r+2位置的字符相同
            else if (s[l] == s[r + 2])
            {
                a[l + 1] = a[l + 2] = a[r] = a[r + 1] = 1;
                a[l] = a[r + 2] = 2;
            }
        }

        cout << 2 << endl;  // 需要2种分配方案
        for (int i = 0; i < s.size(); i++)
        {
            cout << a[i] << " ";  // 输出每个字符的分配方案
        }
        cout << endl;
    }

    return 0;
}

【运行结果】

3 1
3
COWOWCWCO
-1
4
WCOCOWWCOCOW
1
1 1 1 1 1 1 1 1 1 1 1 1
6
COWCOWOWCOWCOWCOWC
2
2 1 1 2 1 1 1 1 2 1 1 2 1 1 2 1 1 2
posted @ 2026-08-29 09:36  团爸讲算法  阅读(7)  评论(0)    收藏  举报