【题目链接】
ybt 1278:【例9.22】复制书稿(book)
洛谷 P1281 书的复制
【题目考点】
1. 动态规划:线性动规
【解题思路】
该题可以抽象为:将由m个数字构成的序列分成k个子段。对于每种子段分割方案,都能求出所有子段中的最大子段和。求所有方案中最大子段和最小的分割子段的方案。
解法1:二分答案
信息学奥赛一本通 1243:月度开销 | OpenJudge NOI 1.11 06:月度开销
本题与上题解决的是同一个问题。
1. 确定二分答案条件
记要确定的最大子段和为x,求最少子段数量的方法:顺序遍历数组,对于每个数字,只要能加入前一个子段后,子段和不超过x,那么就加入前一个子段。否则自己作为下一个子段的第一个元素。(上一题题解中有该贪心方法正确性的证明)。
在最大子段和为x的前提下,将原序列分割成的最少子段的数量如果小于等于k,那么再随意分割一下就可以让子段的数量等于k,因此该最大子段和x就是合理的。
由此得到二分查找的条件为:在最大子段和为x的前提下,将原序列分割成最少子段的数量小于等于k。求满足该条件的最大子段和x的最小值。
2. 输出子段
求出最大子段和x后,由于最终结果要让前面的人少抄写,也就是前面的子段尽可能短。那么可以从后向前遍历,如果当前数字可以加入当前子段(子段和小于等于x),那么加入;否则新开一个子段。这样做后面的子段就会尽可能长,前面的子段会尽可能短。最后用递归的方式倒序输出。
3. 复杂度
二分查找的过程复杂度为
O
(
l
o
g
(
s
u
m
)
)
O(log(sum))
O(log(sum))所有数字加和sum可以认为小于
1
0
9
10^9
109,
l
o
g
2
1
0
9
=
9
∗
l
o
g
2
10
≤
9
∗
l
o
g
2
16
=
36
log_210^9 =9*log_210\le9*log_216=36
log2109=9∗log210≤9∗log216=36,可以忽略不计。
后面输出子段的过程复杂度为
O
(
n
)
O(n)
O(n),因此该方法复杂度为
O
(
n
)
O(n)
O(n)。
解法2:动态规划
1. 状态定义
集合:子段分割方案
限制:看前几个数字,分成几个子段
属性:最大子段和
条件:最小
统计量:最大子段和
状态定义:dp[i][j]
:将前i个数字分成j个子段的方案中最大子段和最小的方案的最大子段和。
初始状态:dp[i][1]
:前i个数字分成1段,就是前i个数字的和
2. 状态转移方程
记数组a的前缀和数组为s,a[i]~a[j]
的子段和为s[j]-s[i-1]
。
集合:将前i个数字分成j个子段的方案
分割集合:根据第j子段的元素个数来分割集合
- 如果第j子段只有
a[i]
,那么将前i个数字分成j个子段的最大子段和最小的方案的最大子段和,为将前i-1个数字分成j-1个子段的最大子段和最小的方案的最大子段和,再与第j子段比较得到的最大子段和。dp[i][j] = max(dp[i-1][j-1], a[i])
。 - 如果第j子段有
a[i-1]
与a[i]
,那么将前i个数字分成j个子段的最大子段和最小的方案的最大子段和,为将前i-2个数字分成j-1个子段的最大子段和最小的方案的最大子段和,再与第j子段比较得到的最大子段和。dp[i][j] = max(dp[i-2][j-1], a[i]+a[i-1])
。
… - 如果第j子段为
a[j]~a[i]
。那么将前i个数字分成j个子段的最大子段和最小的方案的最大子段和,为将前j-1个数字分成j-1个子段的最大子段和最小的方案的最大子段和,再与第j子段的子段和s[i]-s[j-1]
比较得到的最大子段和。dp[i][j] = max(dp[j-1][j-1], s[i]-s[j-1])
。 - 综上,h从j开始增加到i(h最小时前h-1个数字与子段数j-1相等,有
h
=
j
h=j
h=j),取第j子段为
a[h]~a[i]
,有dp[i][j] = max(dp[h-1][j-1], s[i]-s[h-1]))
- 以上所有情况取最小值
3. 输出子段
和解法1中的方法一样
从后向前遍历,如果当前数字可以加入当前子段(子段和小于等于x),那么加入;否则新开一个子段。这样做后面的子段就会尽可能长,前面的子段会尽可能短。最后用递归的方式倒序输出。
4. 复杂度
外层i与j都是从1到500,内层h从j到i。
i | j | 内层循环次数:i-j+1 |
---|---|---|
1 | 1 | 1 |
1 | 2 | 2 |
… | ||
1 | 500 | 500 |
i为1时,总循环次数为 ∑ h = 1 500 h \sum_{h=1}^{500}h ∑h=1500h
i | j | 内层循环次数:i-j+1 |
---|---|---|
2 | 2 | 1 |
2 | 3 | 2 |
… | ||
2 | 500 | 499 |
i为2时,总循环次数为
∑
h
=
1
499
h
\sum_{h=1}^{499}h
∑h=1499h
i从1到500,总循环次数为
∑
i
=
1
500
∑
h
=
1
500
−
i
+
1
h
\sum_{i=1}^{500}\sum_{h=1}^{500-i+1}h
∑i=1500∑h=1500−i+1h,已知
∑
i
=
1
n
=
1
2
(
1
+
n
)
n
\sum_{i=1}^n=\frac{1}{2}(1+n)n
∑i=1n=21(1+n)n,则原式可以化简为:
1
2
∑
i
=
1
500
(
502
−
i
)
(
501
−
i
)
=
1
2
∑
i
=
1
500
(
i
2
−
1003
i
+
251502
)
=
1
2
(
1
3
i
3
−
1003
2
i
2
+
251502
i
)
∣
1
500
=
1
2
(
1
3
50
0
3
−
1003
2
50
0
2
+
251502
∗
500
−
1
3
−
1003
2
+
251502
)
≈
2.11
∗
1
0
7
\frac{1}{2}\sum_{i=1}^{500}(502-i)(501-i) = \frac{1}{2}\sum_{i=1}^{500}(i^2-1003i+251502)=\frac{1}{2}(\frac{1}{3}i^3-\frac{1003}{2}i^2+251502i)|^{500}_1=\frac{1}{2}(\frac{1}{3}500^3-\frac{1003}{2}500^2+251502*500-\frac{1}{3}-\frac{1003}{2}+251502)\approx 2.11*10^7
21∑i=1500(502−i)(501−i)=21∑i=1500(i2−1003i+251502)=21(31i3−21003i2+251502i)∣1500=21(315003−210035002+251502∗500−31−21003+251502)≈2.11∗107
该复杂度可以接受。
【题解代码】
解法1:二分答案
#include <bits/stdc++.h>
using namespace std;
#define N 505
int m, k, a[N];
bool check(int x)//在最大子段和为x的前提下,将原序列分割成最少子段的数量是否小于等于k
{
int sum = 0, ct = 1;//s:每个子段的和 ct:子段数量
for(int i = 1; i <= m; ++i)
{
if(a[i] > x)//如果某个元素大于x,那么不满足前提,条件不满足
return false;
if(sum + a[i] > x)
{
ct++;
sum = a[i];
}
else
sum += a[i];
}
return ct <= k;
}
void show(int i, int x)//输出数组a[1]~a[i]中子段和小于等于x的所有子段左右端点(子段长度从小到大)
{
if(i == 0)
return;
int j, sum = 0;
for(j = i; j >= 1 && sum+a[j] <= x; --j)
sum += a[j];
show(j, x);//输出a[1]~a[j]中的子段
cout << j+1 << ' ' << i << endl;
}
int main()
{
int sum = 0;
cin >> m >> k;
for(int i = 1; i <= m; ++i)
{
cin >> a[i];
sum += a[i];
}
int l = 0, r = sum, mid;
while(l < r)
{
mid = (l+r)/2;
if(check(mid))
r = mid;
else
l = mid+1;
}
show(m, l);//此时l为最小的最大子段和。按要求输出a[1]~a[m]中的所有子段
return 0;
}
解法2:动态规划
#include <bits/stdc++.h>
using namespace std;
#define N 505
int m, k, a[N], s[N];//a:数字序列 s:前缀和
int dp[N][N];//dp[i][j]:将前i个数字分成j个子段的方案中最大子段和最小的方案的最大子段和。
void show(int i, int x)//输出数组a[1]~a[i]中子段和小于等于x的所有子段左右端点(子段长度从小到大)
{
if(i == 0)
return;
int j, sum = 0;
for(j = i; j >= 1 && sum+a[j] <= x; --j)
sum += a[j];
show(j, x);//输出a[1]~a[j]中的子段
cout << j+1 << ' ' << i << endl;
}
int main()
{
cin >> m >> k;
for(int i = 1; i <= m; ++i)
{
cin >> a[i];
s[i] = s[i-1] + a[i];
}
memset(dp, 0x3f, sizeof(dp));//dp初始化为无穷大
for(int i = 1; i <= m; ++i)//前i个数字分成1段,就是前i个数字的和
dp[i][1] = s[i];
for(int i = 1; i <= m; ++i)
for(int j = 2; j <= k; ++j)
for(int h = j; h <= i; ++h)//取子段a[h]~a[i]
dp[i][j] = min(dp[i][j], max(dp[h-1][j-1], s[i]-s[h-1]));
show(m, dp[m][k]);
return 0;
}