题解:学而思编程 数列划分
【题目来源】
学而思编程:数列划分
【题目描述】
给出 \(n\) 项的数列,要求将数列划分成若干段,且每一段之和都不超过 \(s\)。求一共有多少种满足要求的划分方法。
方法数很大,只要求输出除以 \(10^9\) 的余数。
【输入】
第 \(1\) 行,两个正整数 \(n,s\)
第 \(2\) 行,\(n\) 个正整数 \(a_1,a_2,⋯ ,a_n\)
【输出】
一个整数,划分方法数除以 \(10^9\) 的余数。
【输入样例】
5 3
1 2 1 1 1
【输出样例】
10
【核心思想】
-
问题分析:给定长度为 \(n\) 的数列 \(a_1, a_2, \ldots, a_n\),需要将其划分为若干连续段,使得每段的和都不超过 \(s\)。求满足条件的划分方法总数,结果对 \(10^9\) 取模。这是一个线性动态规划问题,需要枚举最后一段的起始位置。
-
算法选择:
- 动态规划(DP):\(dp[i]\) 表示前 \(i\) 个元素的划分方案数
- 前缀和优化:\(sa[i]\) 表示前 \(i\) 个元素的前缀和,用于 \(O(1)\) 计算区间和
-
关键步骤:
- 计算前缀和数组 \(sa[i] = sa[i-1] + a[i]\),用于快速查询区间和
- 初始化:\(dp[0] = 1\)(空数组有 \(1\) 种划分方式)
- 状态转移(从 \(i = 1\) 到 \(n\)):
- 枚举最后一段的起始位置 \(j\)(从 \(i\) 向前遍历到 \(1\))
- 检查区间 \([j, i]\) 的和是否 \(\leq s\):\(sa[i] - sa[j-1] \leq s\)
- 若满足条件:\(dp[i] = (dp[i] + dp[j-1]) \mod MOD\)
- \(dp[j-1]\) 表示前 \(j-1\) 个元素的划分方案数
- 将最后一段 \([j, i]\) 接在这些方案后面,形成新的划分
- 输出 \(dp[n]\) 作为答案
-
时间/空间复杂度:
- 时间复杂度:\(O(n^2)\),外层遍历 \(i\),内层遍历 \(j\),每次区间和查询 \(O(1)\)
- 空间复杂度:\(O(n)\),存储 \(dp\) 数组和前缀和数组 \(sa\)
-
线性DP与区间划分的核心思想:
- 最优子结构:前 \(i\) 个元素的划分方案只与前面位置的方案有关
- 枚举最后一段:通过枚举最后一段的起始位置 \(j\),将问题分解为子问题
- 前缀和优化:将区间和计算从 \(O(n)\) 优化到 \(O(1)\)
- 取模运算:方案数可能很大,每次加法后取模防止溢出
- 适用于序列划分、字符串分割、区间DP等场景
【算法标签】
线性DP-一维
【代码详解】
#include<iostream>
#include<algorithm>
using namespace std;
const int MOD = 1e9; // 模数
int n, s, a[1005], sa[1005], dp[1005]; // n: 数字个数,s: 最大和限制,a: 原数组,sa: 前缀和数组,dp: 动态规划数组
int main()
{
cin >> n >> s; // 读入n和s
for (int i = 1; i <= n; i++)
{
cin >> a[i]; // 读入第i个数字
sa[i] = sa[i - 1] + a[i]; // 计算前缀和
}
dp[0] = 1; // 空子数组有1种划分方式
for (int i = 1; i <= n; i++) // 遍历每个位置
{
for (int j = i; j >= 1; j--) // 向前遍历可能的划分点
{
// 检查从j到i的子数组和是否不超过s
if (sa[i] - sa[j - 1] <= s)
{
// 如果满足条件,将dp[j-1]的方案数加到dp[i]上
dp[i] = (dp[i] + dp[j - 1]) % MOD;
}
}
}
cout << dp[n]; // 输出前n个元素的划分方案总数
return 0;
}
【运行结果】
5 3
1 2 1 1 1
10
浙公网安备 33010602011771号