给定 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;
}