记忆化判断相邻两个原价后的后缀可行性,再从小到大重建字典序最小序列。
OJ: shumeng
题目 ID: CSP201809D
难度:普及+/提高-
标签:动态规划记忆化搜索构造字典序
日期: 2026-07-31 16:21
形式化题目
已知第二天价格序列
思路
朴素递归
把第一天每个价格看作一个选择。已确定
可知
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-31 16:21
* update_at: 2026-08-17 22:41
*/
// brute.cpp:小数据暴力解,按字典序递归枚举每个下一天价格允许的三种原价。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 305;
const int MAX_VALUE = 305;
int n;
int second_price[MAXN]; // 第二天的已知菜价
int first_price[MAXN]; // 正在递归构造的第一天菜价
// 已经确定 a[position-1],a[position],递归尝试 a[position+1] 的所有合法候选。
bool dfs(int position) {
if (position == n) {
return (first_price[n - 1] + first_price[n]) / 2 == second_price[n];
}
int lower = 3 * second_price[position] - first_price[position - 1] - first_price[position];
for (int next = lower; next <= lower + 2; next++) {
if (next < 1 || next >= MAX_VALUE) {
continue;
}
first_price[position + 1] = next;
if (dfs(position + 1)) {
return true;
}
}
return false;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> second_price[i];
}
// 从小到大枚举 a[1],a[2],按字典序找到的第一个完整方案就是答案。
bool found = false;
for (int first = 1; first < MAX_VALUE && !found; first++) {
for (int second = 1; second < MAX_VALUE; second++) {
if ((first + second) / 2 != second_price[1]) {
continue;
}
first_price[1] = first;
first_price[2] = second;
if (n == 2 ? (first + second) / 2 == second_price[2] : dfs(2)) {
found = true;
break;
}
}
}
for (int i = 1; i <= n; i++) {
if (i > 1) {
cout << ' ';
}
cout << first_price[i];
}
cout << '\n';
return 0;
}记忆化后缀可行性
令 possible(i, x, y) 表示已经确定
所有原价格都不超过 302:中间位置相邻价格至少为 1,由三项和至多
从小到大重建
先从小到大枚举满足端点平均值的 possible(2, a1, a2) 为真的二元组。之后每一步也按从小到大枚举三个候选并保留可行后缀,第一个选择必然使整个序列字典序最小。
样例状态转移
下表展示样例选出前缀 2, 2 后的构造。候选区间恰好连续三个整数,最小可行候选被写入下一列。
| 使用的第二天价格 | 已知相邻原价 | 下一项候选 | 选中值 |
|---|---|---|---|
b[2]=2 |
2,2 |
2,3,4 |
2 |
b[3]=1 |
2,2 |
-1,0,1 |
1 |
b[4]=3 |
2,1 |
6,7,8 |
6 |
b[5]=4 |
1,6 |
5,6,7 |
5 |
每个候选都先用 possible 判断是否能满足后续平均值;因此表中保留的最小值不是局部猜测,而是存在完整后缀的最小选择。
代码
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-31 16:21
* update_at: 2026-08-17 22:41
*/
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 305;
const int MAX_VALUE = 305;
int n;
int second_price[MAXN]; // 第二天的已知菜价 b[i]
int first_price[MAXN]; // 构造出来的第一天菜价 a[i]
signed char memo[MAXN][MAX_VALUE][MAX_VALUE]; // memo[i][x][y]:已定 a[i-1]=x,a[i]=y 时后缀是否可行
// 已经确定 a[position-1]=previous、a[position]=current,判断能否继续填出
// a[position+1..n],使 b[position..n] 全部满足。
bool possible(int position, int previous, int current) {
if (position == n) {
return (previous + current) / 2 == second_price[n];
}
signed char &result = memo[position][previous][current];
if (result != -1) {
return result;
}
// 由 b[position] = floor((a[i-1]+a[i]+a[i+1])/3) 反解:
// 下一个原价只可能是 lower、lower+1、lower+2 三个连续整数。
int lower = 3 * second_price[position] - previous - current;
for (int next = lower; next <= lower + 2; next++) {
if (next >= 1 && next < MAX_VALUE && possible(position + 1, current, next)) {
result = 1;
return true;
}
}
result = 0;
return false;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> second_price[i];
}
memset(memo, -1, sizeof(memo));
// 从小到大枚举 a[1],a[2],找第一个能推出完整可行方案的一对。
bool found = false;
for (int first = 1; first < MAX_VALUE && !found; first++) {
for (int second = 1; second < MAX_VALUE; second++) {
if ((first + second) / 2 != second_price[1]) {
continue;
}
if (n == 2) {
if ((first + second) / 2 == second_price[2]) {
first_price[1] = first;
first_price[2] = second;
found = true;
break;
}
} else if (possible(2, first, second)) {
first_price[1] = first;
first_price[2] = second;
found = true;
break;
}
}
}
// 每个位置从小到大尝试三个候选,第一个存在完整后缀的就是字典序最优选择。
for (int position = 2; position <= n - 1; position++) {
int lower = 3 * second_price[position]
- first_price[position - 1] - first_price[position];
for (int next = lower; next <= lower + 2; next++) {
if (next >= 1 && next < MAX_VALUE
&& possible(position + 1, first_price[position], next)) {
first_price[position + 1] = next;
break;
}
}
}
for (int i = 1; i <= n; i++) {
if (i > 1) {
cout << ' ';
}
cout << first_price[i];
}
cout << '\n';
return 0;
}复杂度
状态为位置与两个相邻原价,原价范围至多 302。时间复杂度
总结
逆向平均不需要枚举整个价格序列。局部等式把下一个值压缩为三个选择,记忆化后缀可行性负责避免贪心走进死路;在每个位置优先尝试更小候选即可得到字典序最小解。