CodeForces - 1442D.Sum(分治+dp)

传送门

给定 n n n个不降序列

k k k次操作每次可以取走任意序列的第一个元素

求取走的最大收益


首先如果分别从 a , b a,b a,b数组里取出 x , y x,y x,y个元素,和分别为 s u m a , s u m b suma,sumb suma,sumb

那么设 s u m a > s u m b suma>sumb suma>sumb,那么往 a a a数组后面拿 y y y个一定比 s u m b sumb sumb

所以得出结论,一个数组要么全拿,要么全不拿。

但是因为需要凑够 k k k,所以至多有一个数组取一部分。

我们从前往后做一遍 01 01 01背包为 f f f,从后往前做一遍 01 01 01背包为 g g g

枚举第 i i i个数组作为那个只取前缀的数组,合并 f , g f,g f,g取最大值,复杂度

O ( n k 2 ) O(nk^2) O(nk2),仍然无法通过

发现合并 f , g f,g f,g的复杂度无法接受

如果能直接处理出不选第 i i i个物品的最大价值就好了,合并花的时间会变得很少

于是开始类似线段树那样分治,每一层表示除了 [ l , r ] [l,r] [l,r],其他物品已经都在 01 01 01背包里考虑过了

当我们递归到叶子节点,除了这个节点其他都被考虑过了,于是直接枚举这个物品选择的前缀

这样递归左边的时候,就拿右半部分去做 01 01 01背包

递归右边的时候,就拿左半部分去做 01 01 01背包

这样分治有 l o g 2 n log_2^n log2n层,每层拿 n n n个物品去做 01 01 01背包,复杂度 O ( n k ) O(nk) O(nk)

所以总体复杂度是 O ( n k l o g 2 n ) O(nklog_2^n) O(nklog2n)

使用非常神奇的方式优化了复杂度

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 3009;
int n,k,siz[maxn],a[maxn][maxn];
ll f[maxn],w[maxn],ans;
void solve(int l,int r)
{
	if( l==r )//叶子节点,枚举选多少前缀 
	{
		ll sum = 0;
		for(int i=0;i<=siz[l];i++)	
		{
			sum += a[l][i];
			ans = max( ans,sum+f[k-i] );
		}
		return;
	}
	ll temp[maxn],mid = l+r>>1 ;
	for(int i=1;i<=k;i++)	temp[i] = f[i];
	for(int i=mid+1;i<=r;i++)
	for(int j=k;j>=siz[i];j--)
		f[j] = max( f[j],f[j-siz[i]]+w[i] );
	solve(l,mid);
	for(int i=1;i<=k;i++)	f[i] = temp[i];
	for(int i=l;i<=mid;i++)
	for(int j=k;j>=siz[i];j--)
		f[j] = max( f[j],f[j-siz[i]]+w[i] );
	solve(mid+1,r);
}
int main()
{
	cin >> n >> k;
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&siz[i] );
		for(int j=1;j<=siz[i];j++ )
		{
			int x;	
			if( j<=k )scanf("%d",&a[i][j] );
			else	scanf("%d",&x);
			if( j<=k )	w[i] += a[i][j];
		}
		if( siz[i]>=k )	siz[i] = k;
	}
	solve( 1,n ); 
	cout << ans;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值