打表发现答案由 n mod 7 完全决定:先用最多 7 根木棍的数字确定最短位数,再按「省木棍」模型把七类余数的规律直接构造出来;另给逐位贪心与 DP 预处理两种通用解法。
OJ: luogu
题目 ID: P11229
难度:普及+/提高-
标签:找规律贪心构造DP数学
创建: 2026-07-05 21:24
更新: 2026-10-07 10:45
形式化题目
给定
- 恰好使用
根木棍; - 没有前导零;
- 在满足条件的所有正整数中尽可能小。
如果不存在,输出
数字需要的木棍数量为:
| 数字 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|---|
| 木棍数 | 6 | 2 | 5 | 5 | 4 | 5 | 6 | 3 | 7 | 6 |
解法总览
三个解法共享同一套优先级:先保证位数最短,再保证字典序最小。由此先定下答案位数
- 单个数字最多消耗
根(就是 8),位最多用 根,所以 ; 位的总消耗能取到 里每个整数:从全放 1(根)出发,任选一位换成多耗 根的数字( 每档都有对应数字),一路加到 ;首位不能放 0,但非零数字同样覆盖 每种消耗,所以每位都能自由加 ; - 因此「
位拼得出 」的充要条件是 。上界显然成立;下界由 得 ,即 ( 时 成立, 时 逐个验证); - 边界:
时连一位都拼不出(每个数字至少 根),输出 。
除
- 解法一(
main.cpp,正式主解):打表找规律,直接查 tables[](由 DFS 暴力预先打好),用「 决定前缀,再补 8到位」; - 解法二(
main_greedy.cpp):逐位贪心,每位选最小的、能让剩余木棍填满剩余位数的数字; - 解法三(
main_dp.cpp):DP 预处理每个木棍数的最优「位数 + 首数字」,查询时直接查表输出。
brute.cpp、brute_01_style.cpp、brute-dfs.cpp、brute-dfs-clip.cpp 是打表和对拍用的朴素解,不参与正式解法。
解法一:找规律( 决定前缀)
思路
规律是从打表来的。brute.cpp 用字符串 DP 保存每个木棍数能拼出的最小字符串,适合小数据验证(
// brute.cpp:小数据 DP,保存每个火柴数能拼出的最小字符串。
#include <bits/stdc++.h>
using namespace std;
int cost_digit[10] = {6, 2, 5, 5, 4, 5, 6, 3, 7, 6};
bool better(const string &a, const string &b) {
if (a.empty()) {
return false;
}
if (b.empty()) {
return true;
}
if (a.size() != b.size()) {
return a.size() < b.size();
}
return a < b;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
int n;
cin >> n;
vector<string> dp(n + 1, "");
for (int d = 1; d <= 9; d++) {
if (cost_digit[d] <= n) {
string s = "";
s += char('0' + d);
if (better(s, dp[cost_digit[d]])) {
dp[cost_digit[d]] = s;
}
}
}
for (int sum = 0; sum <= n; sum++) {
if (dp[sum].empty()) {
continue;
}
for (int d = 0; d <= 9; d++) {
int next_sum = sum + cost_digit[d];
if (next_sum > n) {
continue;
}
string s = dp[sum] + char('0' + d);
if (better(s, dp[next_sum])) {
dp[next_sum] = s;
}
}
}
if (dp[n].empty()) {
cout << -1 << '\n';
} else {
cout << dp[n] << '\n';
}
}
return 0;
}另一种更直白的打表方式是 DFS 枚举:dfs(pos, rest) 表示正在决定第 pos 位、还剩 rest 根木棍,每层枚举数字 rest == 0 时把这一整条路径拼成一个数,和当前答案取最小。它没有任何剪枝,只适合很小的
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll n;
ll ans;
// 0 1 2 3 4 5 6 7 8 9
ll stick[] = {6, 2, 5, 5, 4, 5, 6, 3, 7, 6};
ll rcd[100009];
void dfs(ll pos, ll rest) {
if (rest == 1) {
return;
}
if (rest == 0) {
ll num = 0;
for (ll i = 1; i < pos; i++) num = num * 10 + rcd[i];
if (ans == -1 || ans > num) ans = num;
return;
}
ll start = 0;
if (pos == 1) start = 1;
for (ll i = start; i < 10; i++) {
if (rest < stick[i]) continue;
rcd[pos] = i;
dfs(pos + 1, rest - stick[i]);
}
}
int main() {
ll t;
std::cin >> t;
// while (t--) {
// ll n;
// std::cin >> n;
//
// ans = -1;
// dfs(1, n);
// std::cout << ans << "\n";
// }
for (ll i = 1; i <= 30; ++i) {
n = i;
ans = -1;
dfs(1, n);
std::cout << n << " " << ans << "\n";
}
return 0;
}另一种写法:DFS + 位数剪枝(打表 $n=1 \ldots 50$)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll n;
ll ans;
ll ans_len;
// 0 1 2 3 4 5 6 7 8 9
ll stick[] = {6, 2, 5, 5, 4, 5, 6, 3, 7, 6};
ll rcd[100009];
void dfs(ll pos, ll rest) {
if (rest == 1) {
return;
}
if (rest == 0) {
ll num = 0;
for (ll i = 1; i < pos; i++) num = num * 10 + rcd[i];
if (ans == -1 || ans > num) {
ans_len = pos - 1;
ans = num;
}
return;
}
// 剪枝
if (pos > ans_len && ans_len != -1) return;
ll start = 0;
if (pos == 1) start = 1;
for (ll i = 9; i >= start; i--) {
if (rest < stick[i]) continue;
rcd[pos] = i;
dfs(pos + 1, rest - stick[i]);
}
}
// 1 -1
// 2 1
// 3 7
// 4 4
// 5 2
// 6 6
// 7 8
// 8 10
// 9 18
// 10 22
// 11 20
// 12 28
// 13 68
// 14 88
// 15 108
// 16 188
// 17 200
// 18 208
// 19 288
// 20 688
// 21 888
// 22 1088
// 23 1888
// 24 2008
// 25 2088
// 26 2888
// 27 6888
// 28 8888
// 29 10888
// 30 18888
// 31 20088
// 32 20888
// 33 28888
// 34 68888
// 35 88888
// 36 108888
// 37 188888
// 38 200888
// 39 208888
// 40 288888
// 41 688888
// 42 888888
// 43 1088888
// 44 1888888
// 45 2008888
// 46 2088888
// 47 2888888
// 48 6888888
// 49 8888888
// 50 10888888
// 打表
int main() {
ll t;
std::cin >> t;
// while (t--) {
// ll n;
// std::cin >> n;
//
// ans = -1;
// dfs(1, n);
// std::cout << ans << "\n";
// }
for (ll i = 1; i <= 50; ++i) {
n = i;
ans = -1;
ans_len = -1;
dfs(1, n);
std::cout << n << " " << ans << "\n";
}
return 0;
}从打表结果很容易看出规律:8,10 后面跟着一串 8——这两类(恰好是数据范围里的特殊性质 A 和 B)有这么干净的规律,那么其他余数应该也各有规律。
省木棍模型:先假设每位都是 8
8 时共用
根,而且由 8 换成别的数字,就「省」下一些木棍。各数字相对 8 省下的数量:
| 数字 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|---|
| 相对 8 省下 | 1 | 5 | 2 | 2 | 3 | 2 | 1 | 4 | 0 | 1 |
于是问题变成:1 省最多却最小,而 0/6/9 只省
0 <= S - s <= 5 * slots // 每位最多省 5,最少省 0,中间值都取得到由于
每个余数对应的前缀
由上文规则,位数足够时从高位往低位贪心放置,得到的是一段固定的前缀,后面全是 8。
( ):不用省,前缀为空,全是 8;( ):高位放最小的 1省根,下一位放 0再省根凑满,前缀 10;( ): 1一位就省够根,前缀 1;( ):放 1会省根超了,改用次小的 2省根,再用两个 0各省根凑满 ,前缀 200;( ): 2省根,再放 0省根,前缀 20;( ): 2一位省够,前缀2;( ):首位不能放 0,放省根里最小的 6,前缀6。
| 余数 |
0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| 需要省 |
0 | 6 | 5 | 4 | 3 | 2 | 1 |
| 前缀 | 空 | 10 |
1 |
200 |
20 |
2 |
6 |
于是位数足够时,答案就是「前缀 + 若干个 8」:先写下 8 补到
上面的推导默认位数足够放下前缀。位数不够的短答案(例如 tables[] 直接覆盖。
样例理解
tables[18] 得 20 再补一个 8)。规律段取同余数的大数据 20 加 8,得
常量表 + 规律: 查表, 套规律
打表不仅能「看」出规律,还可以把打出来的结果保留下来直接用:tables[]:
ll tables[] = { -1, -1, 1, 7, 4, 2, 6, 8, 10, 18, 22, 20,
28, 68, 88, 108, 188, 200, 208, 288, 688, 888, /* ... */ };tables[] 的数据由独立的 DFS 暴力程序 brute-dfs-clip.cpp 输出,代码里 tables 数组上方的注释也标明了来源。这样两条路径互相独立:小数据完全由暴力背书,大数据靠规律。边界处两者必须吻合:8888888、10888888、18888888(
代码
main.cpp 按 tables[];8 到
/**
* 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-10-02 17:33
* update_at: 2026-10-03 14:21
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// main.cpp:n <= 50 直接查打表数组 tables[],n > 50 按 n = 7k + r 的规律输出。
// tables[] 由 brute-dfs-clip.cpp(DFS 暴力打表)生成。
// tables[i]:恰好用 i 根木棍拼出的最小正整数(下标 0 不用,n = 1 无解记为 -1)
// 由 brute-dfs-clip.cpp 的 DFS 暴力打表得到,范围 n = 1..50
ll tables[] = {
-1, // n = 0(不用)
-1, // n = 1:无解
1, 7, 4, 2, 6, 8, 10, 18, 22, 20,
28, 68, 88, 108, 188, 200, 208, 288, 688, 888,
1088, 1888, 2008, 2088, 2888, 6888, 8888, 10888, 18888, 20088,
20888, 28888, 68888, 88888, 108888, 188888, 200888, 208888, 288888, 688888,
888888, 1088888, 1888888, 2008888, 2088888, 2888888, 6888888, 8888888, 10888888,
}; // tables[50] = 10888888
// 规律:n > 50 时答案位数 len = ceil(n/7),高位放一个固定前缀,其余位全是 8。
// 前缀由 r = n % 7 决定(对应题解的规律表),用字符串直接列出:
// r = 0 -> ""(全 8),r = 1 -> "10",r = 2 -> "1",r = 3 -> "200",
// r = 4 -> "20",r = 5 -> "2",r = 6 -> "6"。
// n > 50 时 len >= 8,像 r = 3 的 "22"、r = 4 的 "4" 这类位数不够的前缀
// 都已经写在 tables[] 里,走不到这里。
const char* prefix[7] = {"", "10", "1", "200", "20", "2", "6"};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
ll T;
cin >> T;
while (T--) {
ll n;
cin >> n;
if (n <= 50) { // 小数据:直接查打表数组
cout << tables[n] << '\n';
continue;
}
// 大数据:固定前缀 + 若干 8,补满 len 位
ll len = (n + 6) / 7; // 答案位数 = ceil(n/7)
ll r = n % 7;
ll done = strlen(prefix[r]); // 前缀已经占用的位数
cout << prefix[r];
for (ll i = done; i < len; i++)
cout << '8';
cout << '\n';
}
return 0;
}复杂度
每次查询
解法二:贪心(逐位构造)
思路
规律虽好,但七类分类是「把答案算出来之后」总结的。如果不打表,也可以直接用通用的逐位贪心:从高位到低位逐位决定数字,每位的选择规则是:
- 第一位不能选
0,后面的位可以选0; - 从小到大尝试数字,选中后剩余的木棍数必须能被剩余位数填满。
由于每位最少用
2 * slots <= rest <= 7 * slots于是每一位从小到大枚举数字,找到第一个满足条件的即可。
样例理解
- 第一位尝试
,用掉 根,剩 根给 位,超过 ,不可行; - 第一位尝试
,用掉 根,剩 根给 位,可行; - 第二位尽量小,选
后剩 根给最后一位,可行; - 最后一位只能选
。答案是 。
代码
/**
* 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-10-02 17:33
* update_at: 2026-10-02 17:33
*/
#include <bits/stdc++.h>
using namespace std;
int cost_digit[10] = {6, 2, 5, 5, 4, 5, 6, 3, 7, 6};
bool can_fill(int rest, int slots) {
return 2 * slots <= rest && rest <= 7 * slots;
}
char choose_digit(int rest, int slots, bool first_digit) {
int start = first_digit ? 1 : 0;
for (int d = start; d <= 9; d++) {
int left = rest - cost_digit[d];
if (can_fill(left, slots - 1)) {
return char('0' + d);
}
}
return '?';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
int n;
cin >> n;
if (n == 1) {
cout << -1 << '\n';
continue;
}
int len = (n + 6) / 7; // 位数越少,正整数越小
int rest = n;
for (int pos = 1; pos <= len; pos++) {
char ch = choose_digit(rest, len - pos + 1, pos == 1);
cout << ch;
rest -= cost_digit[ch - '0'];
}
cout << '\n';
}
return 0;
}复杂度
每次查询
解法三:DP 预处理
思路
如果不习惯用 can_fill 做区间可行性判断,也可以把每个木棍数的最优方案提前 DP 算出来,查询时直接查表输出。
核心思想:对于木棍数
best_0[i]:i 根木棍的最优方案(允许首数字为 0,用于非首位)
best_1[i]:i 根木棍的最优方案(不允许首数字为 0,用于第一位)预处理时对每个
查询时:输出
代码
/**
* 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
* date: 2026-07-07 00:00:00
*/
// main_dp.cpp:DP 预处理每个木棍数的最优(位数,首数字),O(N*10) 预处理 + O(1) 查询。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
int stick[10] = {6, 2, 5, 5, 4, 5, 6, 3, 7, 6};
// best_len_0[i] / best_digit_0[i]:i 根木棍的最优方案(允许首数字为 0,用于非首位)
int best_len_0[MAXN], best_digit_0[MAXN];
// best_len_1[i] / best_digit_1[i]:i 根木棍的最优方案(不允许首数字为 0,用于第一位)
int best_len_1[MAXN], best_digit_1[MAXN];
// 比较 (len_a, digit_a) 是否优于 (len_b, digit_b)
// 更优:位数更少,或位数相同且首数字更小
bool better(int len_a, int digit_a, int len_b, int digit_b) {
if (len_a == 0) return false; // 当前状态无效
if (len_b == 0) return true; // 被比较状态无效
if (len_a != len_b) return len_a < len_b;
return digit_a < digit_b;
}
void precompute() {
for (int i = 2; i < MAXN; i++) {
// 填 best_0:首数字允许为 0
for (int d = 0; d <= 9; d++) {
int cost = stick[d];
if (i < cost) continue;
int rest = i - cost;
if (rest == 1) continue; // 剩余 1 根无法拼出任何数字
int cand_len = (rest == 0) ? 1 : best_len_0[rest] + 1;
int cand_digit = d;
if (better(cand_len, cand_digit, best_len_0[i], best_digit_0[i])) {
best_len_0[i] = cand_len;
best_digit_0[i] = cand_digit;
}
}
// 填 best_1:首数字不允许为 0
for (int d = 1; d <= 9; d++) {
int cost = stick[d];
if (i < cost) continue;
int rest = i - cost;
if (rest == 1) continue;
int cand_len = (rest == 0) ? 1 : best_len_0[rest] + 1;
int cand_digit = d;
if (better(cand_len, cand_digit, best_len_1[i], best_digit_1[i])) {
best_len_1[i] = cand_len;
best_digit_1[i] = cand_digit;
}
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
precompute();
int T;
cin >> T;
while (T--) {
int n;
cin >> n;
if (n == 1 || best_len_1[n] == 0) {
cout << -1 << '\n';
continue;
}
// 输出第一位(由 best_1 确定),剩余用 best_0 递推
int cur = n;
cout << best_digit_1[cur];
cur -= stick[best_digit_1[cur]];
while (cur > 0) {
cout << best_digit_0[cur];
cur -= stick[best_digit_0[cur]];
}
cout << '\n';
}
return 0;
}复杂度
预处理
复杂度对比
| 解法 | 预处理 | 每次查询 | 空间 |
|---|---|---|---|
| 解法一:找规律 | 无 | ||
| 解法二:贪心 | 无 | ||
| 解法三:DP |
总结
本题的优先级顺序是:先保证位数最短,再保证字典序最小。解法一把「每位放 8、总共再省 8」,小数据查 tables[]、大数据走前缀;解法二的区间条件
