Printing Sequences

GitHub跳转原题关系图返回列表

按 PRINT 数量分成 degree 1/2/3,分别检查全相同、块循环和循环体切分。

OJ: usaco

题目 ID: 1493

难度:普及/提高-

标签:递归枚举思维usaco

日期: 2026-07-11 15:14

题意

有一种简单语言:

  • PRINT c 输出一个数字 c
  • REP o ... END 把内部程序重复执行 o 次。

REP 数量不限,但 PRINT 语句最多只能使用 KK 个。给定目标序列,判断能否输出它。

其中 1K31 \leqslant K \leqslant 3

思路

先看一个更贴近程序定义的小数据判断:

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-11 15:14
 * update_at: 2026-07-11 15:17
 */
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 15;
const int MAXK = 3;

int T;
int n, k;
int a[MAXN];
int memo[MAXN][MAXN][MAXK + 1]; // 0 未计算,1 false,2 true。

bool is_period(int l, int r, int len) {
    int total = r - l + 1;
    if (total % len != 0) return false;

    for (int i = l; i <= r; i++) {
        int base = l + (i - l) % len;
        if (a[i] != a[base]) return false;
    }
    return true;
}

bool can_print(int l, int r, int print_limit) {
    if (l > r) return true;
    if (print_limit == 0) return false;

    if (memo[l][r][print_limit] != 0) {
        return memo[l][r][print_limit] == 2;
    }

    if (l == r) {
        memo[l][r][print_limit] = 2;
        return true;
    }

    int len = r - l + 1;

    // 枚举一个 REP 语句:整个区间是否由某个更短程序重复得到。
    for (int body_len = 1; body_len < len; body_len++) {
        if (!is_period(l, r, body_len)) continue;
        if (can_print(l, l + body_len - 1, print_limit)) {
            memo[l][r][print_limit] = 2;
            return true;
        }
    }

    // 枚举程序中语句序列的分界点:左程序 + 右程序。
    for (int mid = l; mid < r; mid++) {
        for (int left_prints = 1; left_prints < print_limit; left_prints++) {
            int right_prints = print_limit - left_prints;
            if (can_print(l, mid, left_prints) &&
                can_print(mid + 1, r, right_prints)) {
                memo[l][r][print_limit] = 2;
                return true;
            }
        }
    }

    memo[l][r][print_limit] = 1;
    return false;
}

void solve_one() {
    cin >> n >> k;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }

    memset(memo, 0, sizeof(memo));

    cout << (can_print(1, n, k) ? "YES" : "NO") << '\n';
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> T;
    while (T--) {
        solve_one();
    }

    return 0;
}

这个暴力用 can_print(l,r,p) 表示区间 [l,r] 能否用不超过 pPRINT 输出。它枚举两种程序结构:两个子程序拼接,或者某个更短程序被 REP 重复。这个模型贴近题意,但正式解法可以利用 K3K \leqslant 3 写得更直接。

把“能用不超过 ddPRINT 输出”称为 degree dd

degree 1

只用一个 PRINT,输出的所有数字必须相同。

所以 check1(l,r) 只需要检查区间是否全相同。

degree 2

两个 PRINT 可以形成两个连续片段,然后这个循环体被 REP 重复。

把序列压缩成连续块,例如:

text
1 1 1 2 2 1 1 1 2 2
=> (1,3), (2,2), (1,3), (2,2)

如果它是 degree 2,那么块序列应该每隔两个重复一次。也就是第 ii 个块要和第 i+2i+2 个块完全相同,块值和块长都要相同。

块数为 1 或 2 时也可以直接输出。

degree 3

K=3K=3,枚举外层 REP 的循环体长度 len。如果整个序列不是由长度为 len 的前缀重复得到,就跳过。

如果找到了一个循环体,还需要判断这个循环体能否拆成:

  • degree 1 + degree 2
  • 或 degree 2 + degree 1

所以再枚举循环体内部的切分点,调用 check1check2 即可。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-11 15:14
 * update_at: 2026-07-11 15:17
 */
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 105;

int T;
int n, k;
int a[MAXN];
int block_value[MAXN], block_len[MAXN];

bool check1(int l, int r) {
    if (l > r) return true;

    for (int i = l + 1; i <= r; i++) {
        if (a[i] != a[l]) return false;
    }
    return true;
}

bool check2(int l, int r) {
    if (l > r) return true;
    if (check1(l, r)) return true;

    int block_cnt = 0;
    for (int i = l; i <= r; i++) {
        if (i == l || a[i] != a[i - 1]) {
            block_cnt++;
            block_value[block_cnt] = a[i];
            block_len[block_cnt] = 1;
        } else {
            block_len[block_cnt]++;
        }
    }

    if (block_cnt <= 2) return true;
    if (block_cnt % 2 == 1) return false;

    for (int i = 1; i + 2 <= block_cnt; i++) {
        if (block_value[i] != block_value[i + 2]) return false;
        if (block_len[i] != block_len[i + 2]) return false;
    }

    return true;
}

bool is_repeated_block(int l, int r, int len) {
    int total = r - l + 1;
    if (total % len != 0) return false;

    for (int i = l; i + len <= r; i++) {
        if (a[i] != a[i + len]) return false;
    }
    return true;
}

bool check3(int l, int r) {
    int total = r - l + 1;

    // 枚举外层 REP 的循环体长度。
    for (int len = 1; len <= total; len++) {
        if (!is_repeated_block(l, r, len)) continue;

        int body_l = l;
        int body_r = l + len - 1;

        // 循环体由 degree 1 + degree 2,或 degree 2 + degree 1 组成。
        for (int cut = 0; cut <= len; cut++) {
            int left_l = body_l;
            int left_r = body_l + cut - 1;
            int right_l = body_l + cut;
            int right_r = body_r;

            if (check1(left_l, left_r) && check2(right_l, right_r)) return true;
            if (check2(left_l, left_r) && check1(right_l, right_r)) return true;
        }
    }

    return false;
}

void solve_one() {
    cin >> n >> k;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }

    bool ok = false;
    if (k == 1) ok = check1(1, n);
    else if (k == 2) ok = check2(1, n);
    else ok = check3(1, n);

    cout << (ok ? "YES" : "NO") << '\n';
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> T;
    while (T--) {
        solve_one();
    }

    return 0;
}

复杂度

N100N \leqslant 100,直接枚举周期长度和切分点可以通过。

check1check2 都是线性的;check3 按官方分析可视为 O(N2)O(N^2) 级别。

空间复杂度为 O(N)O(N)

总结

本题的关键是不要枚举程序文本,而是按 PRINT 数量分析输出序列的结构。

K=1K=1 是全相同,K=2K=2 是块循环,K=3K=3 则枚举外层循环体并把它拆成 degree 1 与 degree 2 两部分。