把记忆转成 B(i) 约束,按区间跳跃贪心构造字典序最小分数序列。
OJ: usaco
题目 ID: 1374
难度:普及+/提高
标签:贪心构造线段树usaco
日期: 2026-07-11 18:45
题意
有 N 头牛,每头牛有一个分数 c[i],范围是 1..C。部分分数已知,未知位置用 0 表示。
FJ 还记得若干条信息 (a,h):第 h 头牛是第一头分数严格大于前 a 头牛最大分数的牛。
要求补全所有未知分数,使所有记忆都成立,并且整个分数序列字典序最小;如果不存在,输出 -1。
思路
先看一个小数据暴力。它按字典序枚举所有未知位置的分数,找到第一个满足所有记忆的序列。
/**
* 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 18:45
* update_at: 2026-07-11 18:49
*/
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 12;
int n, q, c_limit;
int original_c[MAXN], cur_c[MAXN];
int memory_a[MAXN], memory_h[MAXN];
bool found_answer;
bool check_all_memories() {
for (int idx = 1; idx <= q; idx++) {
int a = memory_a[idx];
int h = memory_h[idx];
int max_first_a = 0;
for (int i = 1; i <= a; i++) {
max_first_a = max(max_first_a, cur_c[i]);
}
for (int i = a + 1; i < h; i++) {
if (cur_c[i] > max_first_a) {
return false;
}
}
if (cur_c[h] <= max_first_a) {
return false;
}
}
return true;
}
void dfs_fill(int pos) {
if (found_answer) {
return;
}
if (pos == n + 1) {
if (check_all_memories()) {
found_answer = true;
for (int i = 1; i <= n; i++) {
if (i > 1) {
cout << ' ';
}
cout << cur_c[i];
}
cout << '\n';
}
return;
}
if (original_c[pos] != 0) {
cur_c[pos] = original_c[pos];
dfs_fill(pos + 1);
return;
}
for (int value = 1; value <= c_limit; value++) {
cur_c[pos] = value;
dfs_fill(pos + 1);
if (found_answer) {
return;
}
}
}
void solve_one_case() {
cin >> n >> q >> c_limit;
for (int i = 1; i <= n; i++) {
cin >> original_c[i];
}
for (int i = 1; i <= q; i++) {
cin >> memory_a[i] >> memory_h[i];
}
found_answer = false;
dfs_fill(1);
if (!found_answer) {
cout << -1 << '\n';
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
solve_one_case();
}
return 0;
}满分做法先定义一个函数:
B(i) = 第一头分数严格大于前 i 头牛最大值的牛的位置一条记忆 (a,h) 就是:
B(a) = h如果 i < j < h,也必须有 h 之前还没有出现新的更大分数。
所以每条 (a,h) 会把整个区间 [a,h-1] 的 B 值都约束成 h。若两个约束在同一位置要求不同的 B 值,则无解。
接下来构造分数。所有未知位置先设为 1,这是字典序最小的起点。
处理某个已知
max(c[1..i]) == max(c[1..h-1])c[h] > max(c[1..i])
如果第二段 c[1..h-1] 中已经出现了更大的值,那么必须把某个
但是不能选在某个更小的 B(k) 后面。若存在 B(k) < h,把 c[j] 提高会破坏更早的约束。因此代码用一棵线段树查询前缀中是否存在这样的阻挡位置。
最后,如果 c[h] 原本未知,就把它设成 max(c[1..i])+1;如果原本已知,则检查它是否真的更大。
实现中还会跳过冗余区间:处理完 i+1..h-1 的约束都相同,可以直接跳到 h。
代码
/**
* 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 18:45
* update_at: 2026-07-11 18:49
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 300005;
const int INF = 1000000007;
int n, q;
ll limit_c;
int b_to[MAXN]; // B(i):第一个比前 i 头牛最大值更大的位置
ll c[MAXN]; // 当前构造的分数
bool fixed_score[MAXN]; // 题面中已经给定的分数
int largest_free[MAXN]; // largest_free[i] 表示 <=i 的最大原始未知位置
struct MaxSegTree {
int size;
ll tree[MAXN * 4];
void build(int p, int l, int r) {
if (l == r) {
tree[p] = c[l];
return;
}
int mid = (l + r) / 2;
build(p * 2, l, mid);
build(p * 2 + 1, mid + 1, r);
tree[p] = max(tree[p * 2], tree[p * 2 + 1]);
}
void update(int p, int l, int r, int pos, ll value) {
if (l == r) {
tree[p] = value;
return;
}
int mid = (l + r) / 2;
if (pos <= mid) {
update(p * 2, l, mid, pos, value);
} else {
update(p * 2 + 1, mid + 1, r, pos, value);
}
tree[p] = max(tree[p * 2], tree[p * 2 + 1]);
}
ll query(int p, int l, int r, int ql, int qr) {
if (ql <= l && r <= qr) {
return tree[p];
}
int mid = (l + r) / 2;
ll ans = 0;
if (ql <= mid) {
ans = max(ans, query(p * 2, l, mid, ql, qr));
}
if (qr > mid) {
ans = max(ans, query(p * 2 + 1, mid + 1, r, ql, qr));
}
return ans;
}
} max_tree;
struct MinBSegTree {
int tree[MAXN * 4];
void build(int p, int l, int r) {
if (l == r) {
tree[p] = (b_to[l] == 0 ? INF : b_to[l]);
return;
}
int mid = (l + r) / 2;
build(p * 2, l, mid);
build(p * 2 + 1, mid + 1, r);
tree[p] = min(tree[p * 2], tree[p * 2 + 1]);
}
int find_rightmost(int p, int l, int r, int ql, int qr, int upper) {
if (qr < l || r < ql || tree[p] >= upper) {
return 0;
}
if (l == r) {
return l;
}
int mid = (l + r) / 2;
int right_ans = find_rightmost(p * 2 + 1, mid + 1, r, ql, qr, upper);
if (right_ans != 0) {
return right_ans;
}
return find_rightmost(p * 2, l, mid, ql, qr, upper);
}
} b_tree;
bool fill_b_constraints(vector<pair<int, int> > memories) {
for (int i = 1; i <= n; i++) {
b_to[i] = 0;
}
for (int i = 0; i < (int)memories.size(); i++) {
int a = memories[i].first;
int h = memories[i].second;
b_to[a] = h;
}
int cur = 1;
while (cur <= n) {
int start = cur;
while (cur < b_to[start]) {
if (b_to[cur] != 0 && b_to[cur] != b_to[start]) {
return false;
}
b_to[cur] = b_to[start];
cur++;
}
cur = max(cur, start + 1);
}
return true;
}
void print_answer_or_minus_one(bool ok) {
if (!ok) {
cout << -1 << '\n';
return;
}
for (int i = 1; i <= n; i++) {
if (c[i] > limit_c) {
cout << -1 << '\n';
return;
}
}
for (int i = 1; i <= n; i++) {
if (i > 1) {
cout << ' ';
}
cout << c[i];
}
cout << '\n';
}
void solve_one_case() {
cin >> n >> q >> limit_c;
for (int i = 1; i <= n; i++) {
cin >> c[i];
fixed_score[i] = (c[i] != 0);
if (c[i] == 0) {
c[i] = 1;
}
}
vector<pair<int, int> > memories;
for (int i = 1; i <= q; i++) {
int a, h;
cin >> a >> h;
memories.push_back(make_pair(a, h));
}
if (!fill_b_constraints(memories)) {
cout << -1 << '\n';
return;
}
largest_free[0] = 0;
for (int i = 1; i <= n; i++) {
if (!fixed_score[i]) {
largest_free[i] = i;
} else {
largest_free[i] = largest_free[i - 1];
}
}
max_tree.build(1, 1, n);
b_tree.build(1, 1, n);
bool ok = true;
int i = 1;
while (i <= n && ok) {
if (b_to[i] == 0) {
i++;
continue;
}
int h = b_to[i];
if (h <= i || h > n) {
ok = false;
break;
}
ll max_before = max_tree.query(1, 1, n, 1, i);
ll max_until_before_h = max_tree.query(1, 1, n, 1, h - 1);
if (max_until_before_h > max_before) {
int bad = b_tree.find_rightmost(1, 1, n, 1, i, h);
int pos = largest_free[i];
if (pos <= bad) {
ok = false;
break;
}
c[pos] = max_until_before_h;
max_tree.update(1, 1, n, pos, c[pos]);
max_before = max_until_before_h;
}
if (!fixed_score[h]) {
c[h] = max_before + 1;
max_tree.update(1, 1, n, h, c[h]);
}
if (c[h] <= max_before) {
ok = false;
break;
}
i = h;
}
print_answer_or_minus_one(ok);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
solve_one_case();
}
return 0;
}复杂度
每个位置被处理常数次,线段树查询和修改为
时间复杂度为
总结
本题的关键是把记忆 (a,h) 转成 B 在区间内相同的性质合并约束。
构造时始终从全 1 开始,只在约束逼迫时提高分数,并且尽量提高靠右的未知位置,从而保证字典序最小。