【题解】P5322 [BJOI2019] 排兵布阵(DP,背包)

【题解】P5322 [BJOI2019] 排兵布阵

挺开心的,毕竟这是我为数不多自己做出的蓝题之一。


题目链接

P5322 [BJOI2019] 排兵布阵 - 洛谷

题意概述

这道题的题意很清楚,所以这里直接摘抄原题题面。

小 C 正在玩一款排兵布阵的游戏。在游戏中有 nn 座城堡,每局对战由两名玩家来争夺这些城堡。每名玩家有 mm 名士兵,可以向第 ii 座城堡派遣 aia_i 名士兵去争夺这个城堡,使得总士兵数不超过 mm

如果一名玩家向第 ii 座城堡派遣的士兵数严格大于对手派遣士兵数的两倍,那么这名玩家就占领 了这座城堡,获得 ii 分。

现在小 C 即将和其他 ss 名玩家两两对战,这 ss 场对决的派遣士兵方案必须相同。小 C 通过某些途径得知了其他 ss 名玩家即将使用的策略,他想知道他应该使用什么策略来最大化自己的总分。

由于答案可能不唯一,你只需要输出小 C 总分的最大值。

思路分析

刚开始想了一个前缀和差分套 01 背包的做法。

定义 sumi,jsum_{i,j} 表示对于第 ii 座城堡,派遣士兵 jj 个时,占领了几次城堡。

那么对于一个输入的 ai,ja_{i,j},令 ai,j×2+1=ka_{i,j}\times 2+1=k,然后 sumi,k++sum_{i,k}++

最后对于每一个 ii,求一下 j=1msumi,j\sum \limits_{j=1}^m sum_{i,j},得到新的前缀和数组。

定义 dpi,jdp_{i,j} 表示的是考虑到了第 ii 座城堡,当前派遣士兵总数是 jj 时,得分最大是多少。

可以发现实际上这是一个 01 背包,枚举第 ii 个城堡派遣的士兵个数 kk,那么有:

dp[i][j]=max0kj(dp[i][j],dp[i1][jk]+sum[i][k]i)dp[i][j]=\max \limits_{0 \le k \le j}(dp[i][j],dp[i-1][j-k]+sum[i][k]*i)

直接转移即可。

分析一下时间复杂度:枚举 iiO(n)O(n),枚举 j,kj,k:都是 O(m)O(m)

所以总复杂度:O(nm2)O(nm^2)。复杂度炸没边了。

考虑如何优化。

如果 dpdp 状态不变,那么瓶颈在于枚举 kk 上,因为枚举 i,ji,j 的复杂度都无法改变。

观察数据范围可以发现,mm 的范围虽然很大,但 nn 却很小,能否将枚举 kk 的复杂度降低到 O(n)O(n) 级别呢。

其实是可以的。

既然我们直接枚举士兵个数不成,不妨换个角度。我们枚举可以占领几次城堡,也就是在这个城堡上战胜了多少个敌人。

那么我们只需要知道战胜了 kk 个敌人时的士兵个数即可。

显然选择士兵个数要遵循的原则是:能小则小。即在满足条件的情况下,选最少的士兵。

这个是显然的,因为如果你选的更大就会使得多选的士兵浪费,从而不一定达到最优解。

所以对于战胜了 kk 个敌人的士兵个数,假设第 ii 个敌人派遣到这座城堡的士兵个数是 aia_i,那么答案就是 aa 数组中第 kk 小的元素 ×2+1\times 2+1

那么我们对每一座城堡上所有敌人派遣的士兵个数排序,aka_k 即为第 kk 小。

综上,对于 dpi,jdp_{i,j}

dp[i][j]=max(dp[i1][j],dp[i1][ja[i][k]21]+ki);dp[i][j]=\max(dp[i-1][j],dp[i-1][j-a[i][k]*2-1]+k*i);

ai,ka_{i,k} 表示的是第 ii 个城堡上所有敌人派遣第 kk 小的士兵数量。

时间复杂度:O(n2m)O(n^2m)

易错点

  • 在刚开始输入的时候,由于 ai,ja_{i,j} 表示的是表示的是第 ii 个城堡上所有敌人派遣第 kk 小的士兵数量。但输入相对于这个是反的,所以刚开始应该输入的是 aj,ia_{j,i}

  • 对于一个 dpi,jdp_{i,j},若没有使用滚动数组,则刚开始要继承 dpi1,jdp_{i-1,j}

代码实现

//luoguP5322
#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=105;
const int maxm=2e4+10;
int a[maxn][maxm<<1],dp[maxn][maxm<<1];

inline int read()
{
	int x=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
	return x*f;
}

int main()
{
	int s,n,m;
	s=read();n=read();m=read();
	for(int i=1;i<=s;i++)
	{
		for(int j=1;j<=n;j++)
		{
			int x=read();
			a[j][i]=x;//bug:a[i][j]=x;
		}
	}
	for(int i=1;i<=n;i++)sort(a[i]+1,a[i]+s+1);
	for(int i=1;i<=n;i++)
	{
		for(int j=m;j>=0;j--)
		{
			dp[i][j]=dp[i-1][j];//bug:forget
			//要在枚举 k 的循环外继承。 
			for(int k=1;k<=s;k++)
			{
				if(j>=a[i][k]*2+1)dp[i][j]=max(dp[i][j],dp[i-1][j-a[i][k]*2-1]+k*i);
			}
		}
	}
	int ans=0;
	for(int i=0;i<=m;i++)ans=max(ans,dp[n][i]);
	cout<<ans<<'\n';
	return 0;
}
posted @   向日葵Reta  阅读(146)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· 什么是nginx的强缓存和协商缓存
· 一文读懂知识蒸馏
· Manus爆火,是硬核还是营销?
点击右上角即可分享
微信分享提示