【洛谷P1415】拆分数列【dp】

本文解析洛谷P1415题目,探讨如何通过动态规划求解最优解,实现添加逗号使数字串分割成严格递增序列。文章详细介绍了使用f[i]和g[i]数组寻找最小和最大值解的策略,以及如何处理前导0的问题。

题目大意:

题目链接:https://www.luogu.org/problemnew/show/P1415
给出一列数字,需要你添加任意多个逗号将其拆成若干个严格递增的数。如果有多组解,则输出使得最后一个数最小的同时,字典序最大的解(即先要满足最后一个数最小;如果有多组解,则使得第一个数尽量大;如果仍有多组解,则使得第二个数尽量大,依次类推……)。


思路:

由于要先把最后一位尽量小,考虑先求出最后一位的最小答案。
f [ i ] f[i] f[i]表示在满足前 i i i位的划分单调递增时,以第 i i i位为末,且这一次的划分的值尽量小的时候划分的首位。
简单来说,就是找到一个 j j j满足在 j j j开始划分可以满足单调递增且 j ∼ i ‾ \overline{j\sim i} ji尽量小的方案数。
显然只需要让 j j j尽量大就可以了。就倒序枚举 j j j,如果 f [ j − 1 ] ∼ j − 1 ‾ &lt; j ∼ i ‾ \overline{f[j-1]\sim j-1}&lt;\overline{j\sim i} f[j1]j1<ji就让 f [ i ] = j f[i]=j f[i]=j
其中 x ∼ y ‾ \overline{x\sim y} xy 可以预处理搞定。
接下来求出满足前面尽量大的答案。
g [ i ] g[i] g[i]表示在满足后 i i i位的划分满足单调递增时,以第 i i i位为首,且这一次的划分的值尽量大的时候划分的末位。
显然初始化 d [ f [ n ] ] = n d[f[n]]=n d[f[n]]=n
推方程思路和 f f f基本一样。
注意处理前导0。可以用 s u m [ x ] [ y ] sum[x][y] sum[x][y]表示 x ∼ y ‾ \overline{x\sim y} xy的前导0个数。
这样的时间复杂度就是 O ( n 3 ) O(n^3) O(n3)的。实际上跑起来会小得多。


代码:

#include <cstdio>
#include <string>
#include <iostream>
using namespace std;

const int N=510;
int n,a[N],f[N],g[N],sum[N][N];
string t[N][N];

bool check(int S1,int T1,int S2,int T2)
{
	if (T1-S1-sum[S1][T1]<T2-S2-sum[S2][T2]) return 1;
	if (T1-S1-sum[S1][T1]>T2-S2-sum[S2][T2]) return 0;
	string s1=t[S1+sum[S1][T1]][T1],s2=t[S2+sum[S2][T2]][T2];
	for (int i=0;i<min(s1.size(),s2.size());i++)
	{
		if (s1[i]<s2[i]) return 1;
		if (s1[i]>s2[i]) return 0;
	}
	return 0;
}

void dp1()
{
	f[1]=1;
	for (int i=2;i<=n;i++)
		for (int j=i;j>=1;j--)
			if (check(f[j-1],j-1,j,i))
			{
				f[i]=j;
				break;
			}
}

void dp2()
{
	g[f[n]]=n;
	int k=0;
	for (int i=f[n]-1;!a[i]&&i;i--) g[i]=n,k++;
	for (int i=f[n]-1-k;i>=1;i--)
		for (int j=f[n]-1;j>=i;j--)
			if (check(i,j,j+1,g[j+1]))
			{
				g[i]=j;
				break;
			}
	int i;
}

int main()
{
	while (scanf("%1d",&a[++n])==1)
		for (int i=1;i<=n;i++)
		{
			t[i][n]=t[i][n-1]+(char)(a[n]+48);
			if (sum[i][n-1]==max(n-i,0)&&!a[n]) sum[i][n]=sum[i][n-1]+1;
				else sum[i][n]=sum[i][n-1];
		}
	n--;
	dp1();
	dp2();
	int j=g[1];
	for (int i=1;i<=n;i++)
	{
		printf("%d",a[i]);
		if (i==j&&i!=n)
		{
			j=g[i+1];
			putchar(',');
		}
	}
	return 0;
}

这道题思路还是不难,但是敲了我 3 h 3h 3h。。。恐怖如斯
话说这道题有要求 O ( n log ⁡ n ) O(n\log n) O(nlogn)的加强版 → \to P2282[HNOI2003]历史年份,好像是用 h a s h + hash+ hash+二分判断两个子串的大小,线段树维护f的最大值。。。orz

<think>我们注意到用户的问题是关于“将整数拆分为质数之和”的递归解法,但提供的引用内容都是关于“自然数的拆分问题”(即拆分为正整数之和,且要求输出具体方案)。因此,我们需要调整思路,结合用户之前的问题(拆分为质数之和)和引用中的递归框架来实现。 目标:将正整数n拆分为若干个质数之和,输出所有可能的拆分方案(按字典序输出,每个拆分方案中的质数按非降序排列)。 递归思路: 1. 生成所有不超过n的质数(用于拆分时选择)。 2. 使用深度优先搜索(DFS)进行递归拆分,每次选择一个质数(要求不小于上一个选择的质数,以避免重复且保证升序),然后从剩余的数中继续拆分。 3. 当剩余部分为0时,表示拆分成功,输出方案;当剩余部分小于0时,回溯。 注意:由于质数都是大于等于2的,因此当n=1时无法拆分。 递归函数设计: void dfs(int remain, int start, vector<int>& path, const vector<int>& primes) - remain: 剩余需要拆分的数 - start: 当前可选的质数在primes中的起始下标(保证不降序) - path: 记录当前已选择的质数 - primes: 存储所有不超过n的质数 步骤: a. 如果remain==0,则输出path中的质数(即一个拆分方案),然后返回。 b. 从start开始遍历primes中的质数p: if p > remain: 直接break(因为后面的质数更大,不可能满足) else: 将p加入path,然后递归调用dfs(remain-p, i, path, primes)(注意这里从i开始而不是i+1,因为同一个质数可以重复使用?但题目要求拆分为质数之和,一般每个质数可以重复使用,比如4=2+2。所以允许重复,因此下一个质数可以从当前质数开始选,即start=i,而不是i+1) c. 递归返回后,从path中弹出最后一个质数(回溯)。 但是,注意题目要求:拆分方案中的数字按从小到大排序(即非降序),所以我们在递归时传递的start是当前质数的下标(这样下一个质数不会比当前小),就可以保证非降序。 另外,我们还需要生成质数表。注意:质数表需要包含所有不超过n的质数。 输出要求:按字典序输出。由于我们按质数从小到大的顺序选择,所以自然满足字典序(比如2+3和5,2+3的字典序小于5?但注意拆分方案是多个数,字典序比较规则:先比较第一个数,再比较第二个数,等等。所以2+3应该比5先输出?但2+3的序列是[2,3],5的序列是[5],而2<5,所以[2,3]应该先于[5]输出。而我们的递归是从小质数开始选,因此会先得到2+3,然后得到5,满足要求。 注意:同一个质数可以重复使用,所以递归调用时start传入的是i(当前质数的下标),这样下一次还可以选当前质数。 但是,如果问题要求每个质数只能使用一次,那么递归调用时start传入i+1。根据用户之前的问题描述(拆分为质数之和),通常允许重复使用(比如4=2+2),所以这里我们按照可重复使用来设计。 然而,在引用[1]到[5]中,自然数拆分问题允许重复(如3=1+1+1),但质数拆分问题中,质数重复也是允许的(如4=2+2)。所以我们的递归设计允许重复。 代码实现步骤: 1. 读入n 2. 生成2到n之间的所有质数(因为n可能很大,但n<=10000,所以可以用筛法) 3. 调用dfs(n, 0, path, primes) // 初始start=0,表示从第一个质数开始选 注意:如果n本身是质数,那么单独一个n也是一种拆分。 输出格式:每个拆分方案用加号连接,如2+2+3。 另外,当n<=1时,没有方案,直接返回。 由于n可能较大(但题目没有明确范围,参考引用中自然数拆分问题,n<=8,但质数拆分问题可能要求更大的n),我们需要考虑递归深度。不过,因为质数最小为2,所以递归深度最多为n/2,当n较大时可能栈溢出。但题目没有明确n的范围,我们假设n<=100。 优化:为了避免重复计算质数表,我们可以提前生成好。 代码示例(C++):</think>### 洛谷整数拆分问题递归解法详解 我将为您详细解释洛谷 P2404 自然数拆分问题的递归解法,并扩展质数拆分问题的解决方案。核心思路是使用深度优先搜索(DFS)实现递归拆分,确保拆分方案按非降序排列。 #### 自然数拆分递归算法(洛谷 P2404) ```cpp #include <iostream> #include <vector> using namespace std; // 递归拆分函数 void dfs(int remain, int last, vector<int>& path) { if (remain == 0) { // 成功拆分 cout << path[0]; for (int i = 1; i < path.size(); i++) cout << '+' << path[i]; cout << endl; return; } // 从last开始枚举,保证非降序 for (int i = last; i <= remain; i++) { if (i == 0) continue; // 跳过0 path.push_back(i); // 选择当前数 dfs(remain - i, i, path); // 递归剩余部分 path.pop_back(); // 回溯 } } int main() { int n; cin >> n; vector<int> path; dfs(n, 1, path); // 从1开始拆分 return 0; } ``` ##### 算法要点: 1. **递归函数**:`dfs(remain, last, path)` - `remain`:剩余待拆分的数值 - `last`:上次选择的数(保证非降序) - `path`:存储当前拆分方案 2. **终止条件**:$remain = 0$ 时输出方案 3. **枚举范围**:$i \in [last, remain]$ 保证拆分有序 4. **时间复杂度**:$O(2^n)$(指数级,适合 $n \leq 20$) > 示例:$n=4$ 的拆分方案: > $1+1+1+1$ > $1+1+2$ > $1+3$ > $2+2$ [^1][^2][^3][^4] #### 质数拆分递归算法(扩展) ```cpp #include <iostream> #include <vector> #include <cmath> using namespace std; // 生成≤n的质数表 vector<int> getPrimes(int n) { vector<bool> isPrime(n+1, true); vector<int> primes; for (int i = 2; i <= n; i++) { if (isPrime[i]) { primes.push_back(i); for (int j = i*i; j <= n; j += i) isPrime[j] = false; } } return primes; } // 质数拆分递归 void primeDfs(int remain, int idx, vector<int>& primes, vector<int>& path) { if (remain == 0) { // 成功拆分 cout << path[0]; for (int i = 1; i < path.size(); i++) cout << '+' << path[i]; cout << endl; return; } // 从当前质数开始枚举 for (int i = idx; i < primes.size(); i++) { int p = primes[i]; if (p > remain) break; // 质数太大 path.push_back(p); primeDfs(remain - p, i, primes, path); // 允许重复使用质数 path.pop_back(); } } int main() { int n; cin >> n; vector<int> primes = getPrimes(n); vector<int> path; primeDfs(n, 0, primes, path); return 0; } ``` ##### 算法要点: 1. **质数生成**:埃拉托斯特尼筛法求 $[2, n]$ 的质数 2. **递归控制**:`idx` 参数避免重复方案 3. **边界处理**:$p > remain$ 时提前终止 4. **允许重复**:递归时传递 `i` 而非 `i+1` > 示例:$n=7$ 的质数拆分: > $2+2+3$ > $2+5$ > $7$ > 共3种方案 #### 性能优化策略 1. **记忆化搜索**:存储中间结果避免重复计算 ```cpp unordered_map<string, int> memo; // key: "remain,last" ``` 2. **动态规划**:$O(n^2)$ 解法($n \leq 10^4$) ```cpp vector<int> dp(n+1, 0); dp[0] = 1; for (int i = 1; i <= n; i++) for (int j = i; j <= n; j++) dp[j] += dp[j - i]; ``` 3. **剪枝优化**: - 当 $remain < \text{当前质数}$ 时终止 - 预处理质数表减少重复计算 #### 算法对比 | 特性 | 自然数拆分 | 质数拆分 | |--------------|-----------------------|-----------------------| | **数字范围** | $[1, n]$ | $[2, n]$ 的质数 | | **重复性** | 允许重复 | 允许重复 | | **有序性** | 非降序 | 非降序 | | **复杂度** | $O(2^n)$ | $O(\pi(n)^k)$ | > 其中 $\pi(n)$ 是质数数量,$k$ 是拆分项数
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值