acm-(dp优化、决策单调性、背包)2020ICPC·小米 网络选拔赛第二场 H. Knapsack

题面
传送门
注意到 w w w很小,我们考虑对 w w w相同的物品分成一组,如果要从这一组中取 k k k个物品,显然取价值前 k k k大的物品是最优的,于是我们将重量为 w w w的物品按照价值从大到小排个序,求个前缀和,得到 s u m w [ ] sum_w[] sumw[]数组,如果要从重量为 w w w的物品中选 k k k个的话对应的价值就是 s u m w [ k ] sum_w[k] sumw[k]

d p [ j ] dp[j] dp[j]表示总重量不超过 j j j时的最大价值,现在考虑重量为 i i i的物品如何去更新 d p [ ] dp[] dp[]数组,我们不妨按照 j j j i i i的余数进行分组,假设余数为 r r r的时候,我们更新所有的 d p [ r + k i ] , r + k i ≤ m dp[r+ki],r+ki\le m dp[r+ki],r+kim。方便起见,我们先固定 r , i r,i r,i,设 D P [ k ] = d p [ r + k i ] DP[k]=dp[r+ki] DP[k]=dp[r+ki],那么如何更新 D P [ ] DP[] DP[]数组呢,显然有方程 D P [ j ] = m a x 0 ≤ k ≤ j { l a s t [ k ] + s u m [ k ] [ j ] } DP[j]=max_{0\le k\le j}\{last[k]+sum[k][j]\} DP[j]=max0kj{last[k]+sum[k][j]}成立,其中 l a s t [ ] last[] last[]数组是在考虑重量为 i − 1 i-1 i1的物品的时候更新完毕的 D P DP DP数组, s u m [ k ] [ j ] sum[k][j] sum[k][j]其实就是前面定义的 s u m i [ j − k ] sum_i[j-k] sumi[jk],方便起见简写为 s u m [ k ] [ j ] sum[k][j] sum[k][j]

注意到对于 a < b a<b a<b而言, s u m [ a ] [ b ] + s u m [ a + 1 ] [ b + 1 ] ≥ s u m [ a ] [ b + 1 ] + s u m [ a + 1 ] [ b ] sum[a][b]+sum[a+1][b+1]\ge sum[a][b+1]+sum[a+1][b] sum[a][b]+sum[a+1][b+1]sum[a][b+1]+sum[a+1][b]恒成立,于是满足四边形不等式,于是容易证明 D P DP DP数组的决策点具有单调性,假设 D P [ j ] DP[j] DP[j]的最优决策点为 h [ j ] h[j] h[j],那么有 h [ 0 ] ≤ h [ 1 ] ≤ h [ 2 ] ≤ . . . . h[0]\le h[1]\le h[2]\le .... h[0]h[1]h[2]....。利用决策单调性,我们有两种方法来求解 D P DP DP数组。

法一:我们开一个单调队列来搞,队列中存放决策点即可,对于队首,如果它的值比下一个更小或者队首的决策点到当前点的距离大于 s u m sum sum的尺寸,就pop掉;对于队尾,考虑新加入的决策点是否大于队尾,如果大于就pop掉队尾,此外还要考虑新加入的决策点与队尾的交点是否小于队尾与队尾前一个决策点的交点,如果满足的话也要pop掉队尾。然后我们维护的队列中的队首的决策点一定是最优的,故每次取队首更新 D P DP DP值即可。

法二:利用分治搞,考虑 [ l , r ] [l,r] [l,r]区间的 D P DP DP假设决策点的范围在 [ L , R ] [L,R] [L,R]中,设 m i d mid mid [ l , r ] [l,r] [l,r]的中点,那么扫一遍 [ L , m i n ( R , m i d ) ] [L,min(R,mid)] [L,min(R,mid)]找到最大值来更新 D P [ m i d ] DP[mid] DP[mid],同时记录 h [ m i d ] h[mid] h[mid],于是根据决策单调性,我们容易得到区间 [ l , m i d − 1 ] [l,mid-1] [l,mid1] D P DP DP的最优决策点一定位于 [ L , h [ m i d ] ] [L,h[mid]] [L,h[mid]],而区间 [ m i d + 1 , r ] [mid+1,r] [mid+1,r] D P DP DP的最优决策点一定位于 [ h [ m i d ] , R ] [h[mid],R] [h[mid],R]。递归下去即可。

如果不太理解决策单调性的话参考这篇文章:dp相关优化-决策单调性
这里给出两份代码,分别是法一和法二:

ll dp[maxn],last[maxn],sum[maxn];
vector<ll>w[105];
int q[maxn];//单调队列 
int I,R,SZ;//I,R用来表征dp[R+jI]=max{dp[R+kI]+sum[k][j]}中的两个量;SZ用来表征sum的最大尺寸 
ll f(int x,int j){//决策函数 
	return last[R+x*I]+sum[j-x];
}
bool judge(int a,int b,int c){//判断函数b是否可以弹出队列尾 
	int l=c,r=SZ+a,ans=-1;//r的最大范围为SZ+a
	while(l<=r){//二分找交点,这里的交点是指恰好满足fb>=fc的第一个点 
		int mid=l+r>>1;
		if(f(b,mid)>f(c,mid)){//如果函数c的在mid位置的值小于函数b在mid位置的值说明交点在右边 
			l=mid+1;
		}else r=mid-1,ans=mid;//否则交点在左边
	}
	if(ans==-1 || f(a,ans)<f(b,ans))return false;//如果函数b,c在函数a结束之前不存在交点,或者这个交点在a,b曲线交点之后那么就不需要弹出b 
	return true;//否则弹出b 
}

int main(){
	int n=rd(),m=rd();
	while(n&&m){
		FOR(i,1,101)w[i].clear();
		FOR(i,1,n+1){
			int ww=rd(),v=rd();
			w[ww].push_back(v);
		}
		FOR(i,0,m+1)dp[i]=last[i]=0;
		FOR(i,1,101){
			I=i;
			SZ=w[i].size();
			if(!SZ)continue;
			sort(w[i].begin(),w[i].end(),greater<ll>());
			FOR(j,0,SZ)sum[j+1]=sum[j]+w[i][j];//前缀和 
			FOR(r,0,i){//按余数分类 
				R=r;
				int head=0,tail=0;
				for(int k=r,j=0;k<=m;k+=i,j++){//对同余的所有坐标dp 
					while(head<tail && j-q[head+1]>SZ)head++;//如果超过范围了就弹出队首 
					while(head+1<tail && f(q[head+1],j)<=f(q[head+2],j))head++;//如果队首的函数值被下一个函数超了就弹出即可 
					while(head<tail && f(j,j)>=f(q[tail],j))tail--;//将要加入新的函数,如果这个函数在当前位置的值大于等于队尾在当前位置的值,那么就弹出队尾 
					while(head+1<tail && judge(q[tail-1],q[tail],j))tail--;//比较当前新加入函数与队尾两个函数交点的关系来决定是否弹出队尾 
					q[++tail]=j;//加入新决策函数 
					dp[k]=f(q[head+1],j);
				}
			}
			FOR(j,0,m+1)last[j]=dp[j];
		}
		ll ans=0;
		FOR(i,1,m+1)ans=max(ans,dp[i]);
		wrn(ans);
		n=rd(),m=rd();
	}
}
ll dp[maxn],last[maxn],sum[maxn];
vector<ll>w[105];
int q[maxn];//单调队列 
int I,R,SZ;//I,R用来表征dp[R+jI]=max{dp[R+kI]+sum[k][j]}中的两个量;SZ用来表征sum的最大尺寸 
ll val(int x,int j){ 
	return last[R+x*I]+sum[j-x];
}
void solve(int l,int r,int ml,int mr){
	if(l>r)return;
	int mid=l+r>>1;ll mx=-1;int h=-1;
	for(int i =ml;i<=mid && i<=mr;i++)if(mid-i<=SZ && mx<val(i,mid))mx=val(i,mid),h=i;
	dp[R+mid*I]=mx;
	solve(l,mid-1,ml,h);
	solve(mid+1,r,h,mr);
}

int main(){
	int n=rd(),m=rd();
	while(n&&m){
		FOR(i,1,101)w[i].clear();
		FOR(i,1,n+1){
			int ww=rd(),v=rd();
			w[ww].push_back(v);
		}
		FOR(i,0,m+1)dp[i]=last[i]=0;
		FOR(i,1,101){
			I=i;
			SZ=w[i].size();
			if(!SZ)continue;
			sort(w[i].begin(),w[i].end(),greater<ll>());
			FOR(j,0,SZ)sum[j+1]=sum[j]+w[i][j];//前缀和 
			FOR(r,0,i){//按余数分类
				R=r;
				solve(0,(m-r)/i,0,(m-r)/i);
			}
			FOR(j,0,m+1)last[j]=dp[j]; 
		}
		ll ans=0;
		FOR(i,1,m+1)ans=max(ans,dp[i]);
		wrn(ans);
		n=rd(),m=rd();
	}
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值