二分的妙用

数列分段 Section II

链接:https://www.luogu.com.cn/problem/P1182

题目描述

对于给定的一个长度为 \(N\) 的正整数数列 \(A_{1\sim N}\),现要将其分成 \(M\)\(M\leq N\))段,并要求每段连续,且每段和的最大值最小。

关于最大值最小:

例如一数列 \(4\ 2\ 4\ 5\ 1\) 要分成 \(3\) 段。

将其如下分段:

\[[4\ 2][4\ 5][1] \]

第一段和为 \(6\),第 \(2\) 段和为 \(9\),第 \(3\) 段和为 \(1\),和最大值为 \(9\)

将其如下分段:

\[[4][2\ 4][5\ 1] \]

第一段和为 \(4\),第 \(2\) 段和为 \(6\),第 \(3\) 段和为 \(6\),和最大值为 \(6\)

并且无论如何分段,最大值不会小于 \(6\)

所以可以得到要将数列 \(4\ 2\ 4\ 5\ 1\) 要分成 \(3\) 段,每段和的最大值最小为 \(6\)

输入格式

\(1\) 行包含两个正整数 \(N,M\)

\(2\) 行包含 \(N\) 个空格隔开的非负整数 \(A_i\),含义如题目所述。

输出格式

一个正整数,即每段和最大值最小为多少。

样例 #1

样例输入 #1

5 3
4 2 4 5 1

样例输出 #1

6

提示

对于 \(20\%\) 的数据,\(N\leq 10\)

对于 \(40\%\) 的数据,\(N\leq 1000\)

对于 \(100\%\) 的数据,\(1\leq N\leq 10^5\)\(M\leq N\)\(A_i < 10^8\), 答案不超过 \(10^9\)









解答

  • 用二分答案的思想:这个其实我想到了,但是没法做,当时没思路
  • 忽略了用段数来反映区间和
  • 也就是当前最大值,判断最多构成多少段,如果当前段数比预期小,说明我们选的最大值太大了,如果段数过大,说明我们选的最大值过大了
  • 注意左端点取值,从一个数的最大值开始,也就是将它划分为一段,不能从 0 开始,会被 hack
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int a[N];
int n, m;

bool check(int x)
{
	int cnt = 0;
	LL sum = 0;
	for (int i = 0; i < n; i++)
	{
		if (sum + a[i] <= x) sum += a[i];
		else sum = a[i], cnt++;
	}

    // 注意这个判断
    // 这里取小于,不可取等,因为段数等,可能还有更好的数,过小的,说明数过大
    // 但是这里注意不用减减,因为二分只需保证一个加加或减减,否则用以 TLE
    // 为啥不用考虑,因为下面 else 将这里也考虑了,
    // 就算段数等,也让它加加,找到段数等最大的数,这里 r 不变就体现处作用了
	if (cnt < m) return true;
	else return false;
}

int main()
{
	cin >> n >> m;

	int l = 0, r = 1e9;
	for (int i = 0; i < n; i++)
	{
		scanf("%d", &a[i]);
		l = max(l, a[i]);
	}

	while (l < r)
	{
		int mid = l + r >> 1;
		if (check(mid)) r = mid;
		else l = mid + 1;
	}

	cout << l;
	return 0;
}


说明BUG

input
5 4
1 2 3 4 5

output
5
  • 上述是一个被 hack 的数据,也就是为啥不从 0 或 1 开始,左端点
  • 当时想着虽然现在小,但是不会逐渐增加吗
  • 实际有问题,增加也就是走了 else ,但是存在不走
  • 也就是前面足够小,让段数减的慢,恰好让后面每个数都成为了单独的一段,而这单独的一段又比 mid 大
  • 这就是问题所在了,会让输出比实际输出小
posted @ 2024-04-30 19:45  星竹z  阅读(14)  评论(0编辑  收藏  举报