二分的妙用

星竹z發表於2024-04-30

數列分段 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 大
  • 這就是問題所在了,會讓輸出比實際輸出小

相關文章