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;
} 
posted @ 2024-11-03 21:17  Panda_LYL  阅读(894)  评论(0)    收藏  举报