牛客練習賽14B 區間的連續段

GsjzTle發表於2021-04-26

題目連結

點我跳轉

題目大意

給定一個長度為 \(N\) 的序列 \(A\) 和一個常數 \(K\)

\(M\) 次詢問

每次詢問查詢一個區間 \([L , R]\) 內所有數最少分成多少個連續段

使得每段的和都 \(<= K\) ,若無解則輸出 "\(Chtholly\)"

解題思路

簡單回憶一下倍增求 \(LCA\) 思想:

  • \(f[i][j]\) 表示以 \(i\) 為起點,往上跳 \(i + 2^j\) 步後得到的祖先
  • 因為往上跳 \(2^j\) 等價於先往上跳 \(2^{j - 1}\) 步後再往上跳 \(2^{j - 1}\)
  • 所以可得: \(f[i][j] = f[f[i][j - 1]][j - 1]\)

回到這道題:

暴力的做法即遍歷區間 \([l,r]\) ,貪心的讓每段的長度儘可能大

考慮用倍增思想優化:

定義 \(f[i][j]\) 表示:

\(i\) 為起點,分成 \(2 ^ j\) 個連續段後,所能到達的最遠位置的下一個位置(其中每個段的和都不超過 \(K\)

那麼不難得到: \(f[i][j] = f[f[i][j - 1]][j - 1]\)\(f[i][0]\) 可通過二分字首和得到

然後對於每個詢問 \((L , R)\)

先判斷區間 \([L,R]\) 是否存在 \(A_i\) 使得 \(A_i > K\)

這步我們維護一個權值陣列的字首和 \(O1\) 判斷

即當 \(A_i <= K\) 時,\(sum[i] = sum[i - 1]\)

\(A_i > K\)時,\(sum[i] = sum[i - 1] + 1\)

\(sum[R] - sum[L - 1] > 0\) 則表示該區間存在 \(A_i > K\),直接輸出 \(Chtholly\)

\(sum[R] - sum[L - 1] = 0\) 則從高位往低位列舉 \(j\)

如果 \(f[L][j] > R\) 則表示從 \(L\) 開始劃分出 \(2^j\) 個連續段是 \(OK\)
但是 \(2^j\) 連續段可能太多了(題目要求劃分的連續段個數最少
所以就繼續往下列舉

如果 \(f[L][j] < R\),則表示從 \(L\) 開始劃分出 \(2^j\) 個連續段是不夠的
那就先劃分出 \(2^j\) 個連續段,然後再從 \(f[L][j]\) 的位置繼續劃分
\(ans += 1 << j\)\(L = f[L][j]\)

AC_Code

#include<bits/stdc++.h>

using namespace std;

const int N = 1e6 + 10;

int f[N][22];

int n , m , k , a[N] , sum[N];

long long pre[N];

signed main()
{
	cin >> n >> m >> k;
	
	for(int i = 1 ; i <= n ; i ++)
	{
		cin >> a[i] , pre[i] = pre[i - 1] + a[i];
		
		sum[i] = sum[i - 1] + (a[i] > k); 
	}
	for(int j = 0 ; j <= 21 ; j ++) f[n + 1][j] = n + 1;
	
	for(int j = 0 ; j <= 21 ; j ++)
	{
		for(int i = 1 ; i <= n ; i ++)
		{
			f[i][0] = upper_bound(pre + i , pre + 1 + n , k - a[i] + pre[i]) - pre;
			
			if(!j) continue ;
			
			f[i][j] = f[f[i][j - 1]][j - 1]; 
		}
	}
	while(m --)
	{
		int l , r , ans = 0;
		
		cin >> l >> r;
		
		if(sum[r] - sum[l - 1]) 
		{
			cout << "Chtholly\n";
			
			continue ;
		}
		for(int j = 21 ; j >= 0 ; j --)
		{
			if(f[l][j] - 1 < r) 
			{
				ans += 1 << j;
				
				l = f[l][j];
			}
		}
		cout << ans + 1 << '\n';
	}
	return 0;
}