P11229 [CSP-J 2024] 小木棍 题解
更新日志
upd: 2024/11/29, 增加了 100pts 的 dp 做法.
算法一,dp
首先对于 \(10^5\) 的数据,很明显,如果用 long long 是绝对会爆炸的,所以使用 string 类型进行 dp.
定义状态 \(f_i\),表示用 \(i\) 根木棍能拼出的最小数字.
显然,可以先初始化 1 ~ 7 的情况.
在木棍数相同的情况下,为了让数字最小,我们当然挑数字小的来拼。
最终,1位数的选择方案可以是这样: 0, 1, 2, 4, 6, 7, 8. (0 保留下来是因为只要不是首位,0都可以摆)
状态转移: \(f_i = cmp(f_i, f_{i-stk_j} + pri_j).\),其中, cmp为比较函数,返回数字小的字符串,j为 0 ~ 9 的数字,stk 表示拼当前数字所要用的木棍,pri 为 j 所对应的字符串.
即,在当前数字和少用了 \(stk_j\) 根木棍但在末尾加上数字 j 作比较.
目标: \(f_n\).
时间复杂度为 \(\Theta (TN).\) ( \(T\) 组数据,每次处理 \(8 - n\),有值的跳过.)
但是,因为开 \(10^5\) 大小的 string 类型数组,如果每个位置都给一个初值,是会 MLE 的,所以只好针对 \(1 \le n \le 10^3\) 的数据.
预计得分 \(50pts\).
代码
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
int T;
// 相同木棍数的情况下,数字最小最优
string pri[10] = {"0", "1", "2", "4", "6", "7", "8"}; // 方便 string 处理
int pre[10] = {0, 1, 2, 4, 6, 7, 8};
int stk[10] = {6, 2, 5, 5, 4, 5, 6, 3, 7, 6}; // 使用的木棍数
string f[100005]; // dp数组
/*
f[i] 表示用 i 根木棍能拼出的最小数字
目标: f[n]
转移: f[i] = cmp(f[i], f[i-stk[pre[j]]] + pri[j])
cmp 为自定义比较函数
0 <= j < 7
因为已经保证 f[i-stk[pre[j]]] 最小,所以直接加上 pri[j]
*/
string cmp(string a, string b) {
// 先比较位数,在比较字典序
if (a.size() != b.size()) {
if (a.size() < b.size())
return a;
else if (b.size() < a.size())
return b;
}
if (a.size() == b.size()) {
if (a < b)
return a;
else if (a > b)
return b;
}
return a;
}
void init() { // 初始化
for (int i = 1; i <= 100000; i++)
f[i] = "-1";
}
int main() {
init();
// 先处理 1 ~ 7
f[2] = "1", f[3] = "7", f[4] = "4", f[5] = "2", f[6] = "6", f[7] = "8";
scanf("%d", &T);
// 只有1根木棍无法拼出数字,但其他都可以
while (T--) {
int n;
scanf("%d", &n);
if (n == 1) { // 特判
printf("-1\n");
continue;
}
if (f[n] != "-1") { // 已经处理过了
printf("%s\n", f[n].c_str());
continue;
}
for (int i = 8; i <= n; i++) {
if (f[i] != "-1") continue; // 有解
for (int j = 0; j < 7; j++) {
if (f[i] == "-1") // 还没有初值
f[i] = f[i-stk[pre[j]]] + pri[j];
else // 转移
f[i] = cmp(f[i], f[i-stk[pre[j]]] + pri[j]);
}
}
printf("%s\n", f[n].c_str());
}
return 0;
}
正解
通过题目中特殊性质可以大概推断出,答案可能跟 \(n\) \(mod\) \(7\) 的值有关.
木棍个数 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
最小数字 | -1 | 1 | 7 | 4 | 2 | 6 | 8 | 10 | 18 | 22 | 20 | 28 | 68 | 88 |
就根据这前 \(14\) 个,我们可以得出,答案 一定 跟 \(n\) \(mod\) \(7\) 的值有关系.
下面是结论.
\(n=1\) 无法拼出
\(2 \le n \le 7\),预处理即可.
\(n\) % \(7=0\)
\(n \div 7\) 个 \(8\).
\(n\) % \(7=1\)
"10" 开头,\((n-8) \div 7\) 个 \(8\).
\(n\) % \(7=2\)
"1" 开头,\((n-1) \div 7\) 个 \(8\).
\(n\) % \(7=3\)
\(n = 10\),答案为 \(22\).
否则,为 "200" 开头,\((n-17) \div 7\) 个 \(8\).
\(n\) % \(7=4\)
"20" 开头,\((n-11) \div 7\) 个 \(8\).
\(n\) % \(7=5\)
"2" 开头, \((n-5) \div 7\) 个 \(8\).
\(n\) % \(7=6\)
"6" 开头, \((n-6) \div 7\) 个 \(8\).
代码
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
int t;
int main() {
scanf("%d", &t);
while (t--) {
int n;
scanf("%d", &n);
if (n == 1) {
printf("-1");
}
else if (n == 2)
printf("1");
else if (n == 3)
printf("7");
else if (n == 4)
printf("4");
else if (n == 5) {
printf("2");
}
else if (n == 6) {
printf("6");
}
else if (n == 7)
printf("8");
if (n >= 1 && n <= 7) {
printf("\n");
continue;
}
if (n % 7 == 1) {
printf("10");
n -= 8;
}
else if (n % 7 == 2) {
printf("1");
n -= 2;
}
else if (n % 7 == 3) {
if (n == 10) {
printf("22\n");
continue;
}
printf("200");
n -= 17;
}
else if (n % 7 == 4) {
printf("20");
n -= 11;
}
else if (n % 7 == 5) {
printf("2");
n -= 5;
}
else if (n % 7 == 6) {
printf("6");
n -= 6;
}
for (int i = 1; i <= n / 7; i++)
printf("8");
printf("\n");
}
return 0;
}
算法三 dp + 滚筒数组
dp[i] 表示用 i 根木棍拼出的最小数字
首先拼1个数字要用 2 ~ 7 根木棍,
i % 8 就可以处理 0 ~ 7 根木棍,所以可以开滚筒数组节省空间
初始化:
dp[i%8] = "0";
dp[i%8] = dp[(i - 2 + 8) % 8] + sum[2];
首先先把答案初始化为 0, (i - 2 + 8) % 8 是用掉 i - 2 根木棍所拼出的最小数字
\(- 2\) 是因为这 2 根木棍拼出数字 1,也就是我们当前认为在后面添 1 能拼出最小数字
\(+ 8\) % 8 是为了时数组中的下标不是负数
转移:
if (dp[j%8] != "-1")
dp[i%8] = cmp(dp[i%8], dp[j%8] + sum[i-j]);
\(i - 7 \le j \le i - 2 且 j \ge 0\)
因为拼1个数字要用 2 ~ 7 根木棍,所以就在 \(i - 7\) ~ \(i - 2\)
cmp 为比较函数,返回数字小的字符串,先比较字符串长度,在比较字典序
如果 dp[j%8] 能拼出数字,那么 dp[i%8] 在 dp[j%8] 末尾加上 sum[i-j] 和自身 做比较,取数字小的那一个
下标为 \(i-j\) 是因为当我们只拼了 j 根时,并且 \(j \le i\),所以还剩下了 \(i-j\) 根,要加在数字末尾
但是有个特殊情况,就是 i - j = 6,因为 6 根木棍既可以拼 数字6,也可以拼数字0(不在首位的情况),所以我们要特判一下
if (dp[j%8] != "-1" && dp[j%8] != "0" && i - j == 6)
dp[i%8] = cmp(dp[i%8], dp[j%8] + "0");
如果 dp[j%8] 能拼出数字,并且它不是只有 1 个 0,即首位为 0 或 数字为 0 并且 i - j = 6,即可以拼 数字0
那么 dp[i%8] 在 dp[j%8] 末尾加上 数字0 和自身 做比较,取数字小的那一个
处理完后,我们要更新答案,因为我们提前记录每个问题,所以只要在 T 个问题中找有没有一样的题目, 即木棍数相同,然后将答案记录在 res 中
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
// 预处理 0 ~ 7
string sum[15] = {"-1", "-1", "1", "7", "4", "2", "6", "8"};
string dp[15];
string res[55];
int T, Q[55], N;
/*
solve函数主要功能:
dp[i] 表示用 i 根木棍拼出的最小数字
首先拼1个数字要用 2 ~ 7 根木棍,
i % 8 就可以处理 0 ~ 7 根木棍,所以可以开滚筒数组节省空间
初始化:
dp[i%8] = "0";
dp[i%8] = dp[(i - 2 + 8) % 8] + sum[2];
首先先把答案初始化为 0, (i - 2 + 8) % 8 是用掉 i - 2 根木棍所拼出的最小数字
- 2 是因为这 2 根木棍拼出数字 1,也就是我们 当前认为 在后面添 1 能拼出最小数字
+ 8 % 8 是为了时数组中的下标不是负数
转移:
if (dp[j%8] != "-1")
dp[i%8] = cmp(dp[i%8], dp[j%8] + sum[i-j]);
(i - 7 <= j <= i - 2,j 还要 >= 0)
因为拼1个数字要用 2 ~ 7 根木棍,所以就在i - 7 ~ i - 2
cmp 为比较函数,返回数字小的字符串,先比较字符串长度,在比较字典序
如果 dp[j%8] 能拼出数字,那么 dp[i%8] 在 dp[j%8] 末尾加上 sum[i-j] 和自身 做比较,取数字小的那一个
下标为 i - j 是因为当我们只拼了 j 根时,并且 j <= i,所以还剩下了 i - j 根,要加在数字末尾
但是有个特殊情况,就是 i - j = 6,因为 6 根木棍既可以拼 数字6,也可以拼数字0(不在首位的情况)
所以我们要特判一下
if (dp[j%8] != "-1" && dp[j%8] != "0" && i - j == 6)
dp[i%8] = cmp(dp[i%8], dp[j%8] + "0");
如果 dp[j%8] 能拼出数字,并且它不是只有 1 个 0,即首位为 0 或 数字为 0
并且 i - j = 6,即可以拼 数字0
那么 dp[i%8] 在 dp[j%8] 末尾加上 数字0 和自身 做比较,取数字小的那一个
处理完后,我们要更新答案,因为我们提前记录每个问题,所以只要在 T 个问题中找有没有一样的
题目, 即木棍数相同,然后将答案记录在 res 中
*/
string cmp(string x, string y) {
if (x.size() != y.size())
return (x.size() > y.size()) ? y : x;
return x > y ? y : x;
}
void solve(int n) {
dp[0] = dp[1] = "-1";
for (int i = 2; i <= n; i++) {
dp[i%8] = "0";
dp[i%8] = dp[(i - 2 + 8) % 8] + sum[2];
if (i <= 7) dp[i] = sum[i];
for (int j = i - 2; j >= i - 7 && j >= 0; j--) {
if (dp[j%8] != "-1")
dp[i%8] = cmp(dp[i%8], dp[j%8] + sum[i-j]);
if (dp[j%8] != "-1" && dp[j%8] != "0" && i - j == 6)
dp[i%8] = cmp(dp[i%8], dp[j%8] + "0");
}
for (int j = 1; j <= T; j++) // 在 T 个问题中更新答案
if (Q[j] == i) res[j] = dp[i%8];
}
}
int main() {
scanf("%d", &T);
for (int i = 1; i <= T; i++) {
scanf("%d", &Q[i]);
N = max(N, Q[i]);
}
solve(N);
for (int i = 1; i <= T; i++) {
if (res[i] == "") printf("-1\n");
else printf("%s\n", res[i].c_str());
}
return 0;
}