先用最多 7 根火柴的数字确定最短位数,再逐位选择能让剩余火柴可填满的最小数字。
OJ: luogu
题目 ID: P11229
难度:普及/提高-
标签:贪心构造DP数学
日期: 2026-07-05 21:24
题意
给定
- 恰好使用
根木棍; - 没有前导零;
- 在满足条件的所有正整数中尽可能小。
如果不存在,输出
数字需要的木棍数量为:
| 数字 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|---|
| 木棍数 | 6 | 2 | 5 | 5 | 4 | 5 | 6 | 3 | 7 | 6 |
思路
先看一个可以直接验证想法的朴素解:
// 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;
}下面是另一种「按位构造」写法。它从左到右决定每一位放哪个数字,并用剩余木棍数判断后续是否还能填满:
另一种写法:按位构造
// brute_01_style.cpp:按位构造写法,把每一位放哪个数字看成一层决策。
#include <bits/stdc++.h>
using namespace std;
int cost_digit[10] = {6, 2, 5, 5, 4, 5, 6, 3, 7, 6};
int total_len;
string answer;
// 判断剩余 rest 根火柴能否填满 slots 个数位。
bool can_fill(int rest, int slots) {
if (rest < 0) {
return false;
}
return 2 * slots <= rest && rest <= 7 * slots;
}
bool dfs_build(int pos, int rest) {
if (pos == total_len) {
return rest == 0;
}
int slots_left = total_len - pos - 1;
int start_digit = (pos == 0 ? 1 : 0);
// 从小到大尝试数字,第一次成功就是当前长度下的最小数。
for (int d = start_digit; d <= 9; d++) {
int left = rest - cost_digit[d];
if (!can_fill(left, slots_left)) {
continue;
}
answer[pos] = char('0' + d);
if (dfs_build(pos + 1, left)) {
return true;
}
}
return false;
}
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;
}
total_len = (n + 6) / 7; // 位数越少,正整数越小。
answer.assign(total_len, '0');
if (dfs_build(0, n)) {
cout << answer << '\n';
} else {
cout << -1 << '\n';
}
}
return 0;
}brute.cpp 用 DP 保存每个木棍数能拼出的最小字符串,适合小数据验证,但
要让正整数尽可能小,第一优先级是位数尽可能少。因为任意
对于
确定了最少位数后,以下是两种实现方式。
解法一:贪心(逐位构造)
从高位到低位逐位决定数字。每位的选择规则:
- 第一位不能选
,后面的位可以选 ; - 从小到大尝试数字,选中后剩余的木棍数必须能被剩余位数填满。
由于每位最少用
2 * slots <= rest <= 7 * slots于是每一位从小到大枚举数字,找到第一个满足条件的即可。
样例理解
- 第一位尝试
,用掉 根,剩 根给 位,超过 ,不可行; - 第一位尝试
,用掉 根,剩 根给 位,可行; - 第二位尽量小,选
后剩 根给最后一位,可行; - 最后一位只能选
。答案是 。
#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 预处理
如果不习惯用
核心思想:对于木棍数
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;
}复杂度
- 解法一:
每次查询, 空间 - 解法二:预处理
,查询 , 空间
总结
本题的贪心顺序是:先保证位数最短,再保证字典序最小。解法一的区间条件
