CF 1400E Clear the Multiset(贪心,分治,DP)

传送门

题目大意

  给你一个有 n n n个元素的集合,第 i i i个元素有 a i a_i ai个,有两个操作:1.取走值从l到r的元素各一个。2.取走值为i的元素任意x个。问最少执行多少次操作把集合内所有元素取完。

解题思路

思路一:贪心 最坏 O ( n 2 ) O(n^2) O(n2),但实际上非常高效
   可以发现在对某个区间执行操作1时,一定要反复执行直到该区间中的一个值的个数为0,这样的操作1才是有意义的。因此可以采用分治思想,对于一个区间 l , r l, r l,r,要么全用操作2将区间内所有数取走(一共 r − l + 1 r - l + 1 rl+1次操作2),要么对整个区间使用操作1,直到某个数个数变为0,并且把整个序列按0分段当作子问题递归处理。

思路二:DP O ( n 2 ) O(n^2) O(n2)
  通过分析可以发现操作2最好不要在相同元素上使用超过一次。这样设 f [ i ] [ j ] f[i][j] f[i][j]表示第 i i i种元素用操作1取出 j j j个所用的操作总个数。转移: (如果前面使用了k次操作1取出了 k k k i − 1 i - 1 i1)

  • k < j k < j k<j时,我们可以让j中的k个跟着前面的一起取走,而剩下的就是增加的代价,如果 j < a [ i ] j < a[i] j<a[i]还要使用一次操作2将其变为j个
    f [ i ] [ j ] = min ⁡ { f [ i − 1 ] [ k ] + ( j − k ) + I ( j < a [ i ] ) } = min ⁡ { f [ i − 1 ] [ k ] − k } + j + I ( j < a [ i ] ) f[i][j] = \min \{f[i-1][k] + (j - k) + \mathbb I (j < a[i]) \} = \min\{f[i-1][k] - k\}+j + \mathbb I (j < a[i]) f[i][j]=min{f[i1][k]+(jk)+I(j<a[i])}=min{f[i1][k]k}+j+I(j<a[i])
  • j ≤ k j \leq k jk时, 我们可以让所有的j个跟着前面的一起被取走不需要代价,如果 j < a [ i ] j < a[i] j<a[i]还要使用一次操作2将其变为j个 f [ i ] [ j ] = min ⁡ { f [ i − 1 ] [ k ] + I ( j < a [ i ] ) } = min ⁡ { f [ i − 1 ] [ k ] } + I ( j < a [ i ] ) f[i][j] = \min\{ f[i-1][k] +\mathbb I (j < a[i]) \} = \min \{f[i - 1][k]\} + \mathbb I (j < a[i]) f[i][j]=min{f[i1][k]+I(j<a[i])}=min{f[i1][k]}+I(j<a[i])

(如果理解不了,可以想一下只考虑用操作1不考虑用操作2,如何求操作次数)

   再考虑优化转移,第一种情况中的 min ⁡ { f [ i − 1 ] [ k ] − k } \min\{f[i-1][k] - k\} min{f[i1][k]k}可以通过维护前缀最小值来解决。第二种转移直接通过维护后缀最小值解决 min ⁡ { f [ i − 1 ] [ k ] } \min \{f[i - 1][k]\} min{f[i1][k]},这样转移就是 O ( 1 ) O(1) O(1)的了。

代码实现

具体细节请参考代码:
思路一:(50ms)

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 3e5 + 5;

int a[MAXN], n;
int solve(int l, int r) {
	if (r < l) return 0;
	int p = *min_element(a + l, a + r + 1);
	int t = l, ans = p;
	for (int i = l; i <= r; i++) {
		a[i] -= p;
		// 按 0 分段计算
		if (!a[i]) ans += solve(t, i - 1), t = i + 1;
	}
	ans += solve(t, r);
	return min(ans, r - l + 1);		
}
int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) scanf("%d", a + i);
	printf("%d\n", solve(1, n));
	return 0;
}

思路二:(250ms)

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 5e3 + 5;
 
int f[MAXN][MAXN], mi[MAXN], a[MAXN], n;
int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) scanf("%d", a + i);
	for (int i = 0; i <= n; i++) 
		for (int j = 0; j <= n; j++) 
			f[j][i] = 1e9;
	mi[n + 1] = 1e9; f[0][0] = 0;
	for (int i = 1; i <= n; i++) {
		int c = 1e9;
		for (int j = n; j >= 0; j--) mi[j] = min(f[i - 1][j], mi[j + 1]); // 维护后缀最小值
		for (int j = 0; j <= min(a[i], n); j++) {
			c = min(c, f[i - 1][j] - j); // 维护前缀最小值
			f[i][j] = min(mi[j], c + j) + (j < a[i]);
		}
	}
	int ans = 1e9;
	for (int j = 0; j <= min(a[n], n); j++) 
		ans = min(ans, f[n][j]);
	cout << ans << endl;
	return 0; 
} 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值