Codeforces Round 938 (Div. 3)(A,B,C,D,E,F,G,H)

题目链接

该死的调休,这几天基本都是满课,要么就是两三场比赛打满,根本补不完题,马上周末又是一堆比赛。最近CF不知道在抽什么风,动不动就要验我是不是机器人,然后转圈圈,再返回一个 “Oops!”。提交界面是打不开的,评测结果是 in queue 的,题目是卡时间的,人是忙里偷闲的,rating分是蹭蹭往下掉的。题解尽量肝,肝不出来就先丢 to do list 了。

这场做了4道就润了,有早八不敢熬了。后面的题基本都是看这个题解补的。

在这里插入图片描述

这场总体来说还是比较简单的,但是考点比较新奇。D大概是个滑动窗口思想,然后相邻窗口进行递推。E是个差分优化的暴力。F是个博弈,或者够强可以直接guess结论。G是个dp,但是这个dp值需要存储所有可能的约数,最后优化成对所有约数进行暴力验证。H是个状压dp,不太好想,写起来也挺暴力的。


A. Yogurt Sale

题意:

在 "Vosmiorochka "商店,一份酸奶的价格是 a a a 布尔,但现在有促销活动,可以用 b b b 布尔购买两份酸奶。

马克西姆需要购买的酸奶数量正好是 n n n 。在购买两份酸奶时,他可以选择以正常价格或促销价格购买。

马克西姆购买 n n n 份酸奶至少需要花费多少布尔?

思路:

如果买两份酸奶的时候促销价格更便宜,那就促销价格买,否则就用正常价格。可能剩下一个,正常价格买。

code:

#include <iostream>
#include <cstdio>
using namespace std;

int T,n,a,b;

int main(){
	cin>>T;
	while(T--){
		cin>>n>>a>>b;
		if(2*a>b)cout<<n/2*b+n%2*a<<endl;
		else cout<<n*a<<endl;
	}
	return 0;
}

B. Progressive Square

题意:

大小为 n n n 的累进正方形是一个 n × n n \times n n×n 矩阵。马克西姆选择三个整数 a 1 , 1 a_{1,1} a1,1 c c c d d d ,并根据以下规则构造一个累进正方形:

a i + 1 , j = a i , j + c a_{i+1,j} = a_{i,j} + c ai+1,j=ai,j+c

a i , j + 1 = a i , j + d a_{i,j+1} = a_{i,j} + d ai,j+1=ai,j+d

例如,如果 n = 3 n = 3 n=3 a 1 , 1 = 1 a_{1,1} = 1 a1,1=1 c = 2 c=2 c=2 d = 3 d=3 d=3 ,那么累进正方形如下:

( 1 4 7 3 6 9 5 8 11 ) \begin{pmatrix} 1 & 4 & 7 \\\\ 3 & 6 & 9 \\\\ 5 & 8 & 11 \end{pmatrix} 1354687911

上个月,马克西姆构建了一个累进正方形,并记住了 n n n c c c d d d 的值。最近,他发现了 n 2 n^2 n2 个整数,并想确认这些整数确实是个特定累进正方形的元素。

可以证明,对于 n n n a 1 , 1 a_{1,1} a1,1 c c c d d d 中的任何值,都存在一个满足所有规则的渐进正方形。

思路:

这个题的 n n n 很小,而且 c , d c,d c,d 居然告诉你了。

正着去验证 n ∗ n n*n nn 个元素是累进正方形的元素似乎比较困难,但是发现累进正方形左上角的元素一定是最小的,因此我们可以直接找到 a 1 , 1 a_{1,1} a1,1 的值,再加上 c , d c,d c,d 知道,我们直接就知道了累进正方形是什么,排序后每个元素一一验证一下就行。

code:

#include <iostream>
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;

int T,n,c,d;

int main(){
	cin>>T;
	while(T--){
		cin>>n>>c>>d;
		vector<int> a(n*n),b;
		for(int i=0,t;i<n*n;i++){
			cin>>t;
			a[i]=t;
		}
		sort(a.begin(),a.end());
		for(int i=0;i<n;i++)
			for(int j=0;j<n;j++)
				b.push_back(a[0]+i*c+j*d);
		sort(b.begin(),b.end());
		
		bool flag=false;
		for(int i=0;i<n*n;i++)
			if(a[i]!=b[i]){
				flag=true;
				break;
			}
		puts((flag)?"NO":"YES");
	}
	return 0;
}

C. Inhabitant of the Deep Sea

题意:

n n n 艘飞船出发探索海洋深处。这些飞船的编号从 1 1 1 n n n ,依次递增;第 i i i 艘飞船的耐久度为 a i a_i ai

克拉肯按照特定的顺序攻击了这些飞船 k k k 次。首先,它攻击第一艘飞船,然后是最后一艘,接着又是第一艘,依此类推。

克拉肯的每次攻击都会使飞船的耐久度降低 1 1 1 。当船只的耐久度下降到 0 0 0 时,它就会沉没,不再受到攻击(因此船只不再是第一艘或最后一艘,克拉肯只攻击尚未沉没的船只)。如果所有的船只都沉没了,克拉肯也就没有什么可攻击的了,于是它就游走了。

例如,如果 n = 4 n=4 n=4 k = 5 k=5 k=5 a = [ 1 , 2 , 4 , 3 ] a=[1, 2, 4, 3] a=[1,2,4,3] ,就会发生以下情况:

  1. 卡拉基攻击第一艘飞船,它的耐久度变为零,现在为 a = [ 2 , 4 , 3 ] a = [2, 4, 3] a=[2,4,3]
  2. 卡拉基攻击最后一艘飞船,现在为 a = [ 2 , 4 , 2 ] a = [2, 4, 2] a=[2,4,2]
  3. 克拉肯攻击第一艘飞船,现在为 a = [ 1 , 4 , 2 ] a = [1, 4, 2] a=[1,4,2]
  4. 克拉肯号攻击最后一艘船,现在是 a = [ 1 , 4 , 1 ] a = [1, 4, 1] a=[1,4,1]
  5. 克拉肯攻击第一艘飞船,其耐久度变为零,现在为 a = [ 4 , 1 ] a = [4, 1] a=[4,1]

克拉肯攻击后有多少艘船被击沉?

思路:

朴素的思路是直接模拟这个过程,但是 k ≤ 1 0 15 k\le 10^{15} k1015,会T。

考虑到如果他总的攻击次数不能击穿所有飞船(也就是 k k k 比所有飞船总的耐久值都高),那么中间一定会剩下一些飞船,而两边受到的伤害其实是差不多的(前面受到 ⌈ k 2 ⌉ \left\lceil\dfrac k2\right\rceil 2k 伤害,后面受到 ⌊ k 2 ⌋ \left\lfloor\dfrac k2\right\rfloor 2k 伤害)。

所以我们可以把这种割裂的攻击方式转化成比较集中的攻击方式。我们把打一点伤害就换边的方式换成前面打 ⌈ k 2 ⌉ \left\lceil\dfrac k2\right\rceil 2k 伤害,后面打 ⌊ k 2 ⌋ \left\lfloor\dfrac k2\right\rfloor 2k 伤害的方式就可以了,这样一次可以直接判断能不能打掉一个飞船,时间复杂度为 O ( n ) O(n) O(n)

code:

#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll;
const int maxn=2e5+5;

int T,n;
ll k;
int a[maxn];

int main(){
	cin>>T;
	while(T--){
		cin>>n>>k;
		ll tot=0,pre=(k+1)/2,suf=k/2;
		for(int i=1;i<=n;i++)cin>>a[i],tot+=a[i];
		if(tot<=k){
			cout<<n<<endl;
			continue;
		}
		
		int l,r;
		for(l=1;l<=n;l++)
			if(pre>=a[l])pre-=a[l];
			else {
				pre=0;
				break;
			}
		for(r=n;r>=1;r--)
			if(suf>=a[r])suf-=a[r];
			else {
				suf=0;
				break;
			}
		cout<<n-(r-l+1)<<endl;
	}
	return 0;
}

D. Inaccurate Subsequence Search

题意:

Maxim 有一个由 n n n 个整数组成的数组 a a a 和一个由 m m m 个整数组成的数组 b b b ( m ≤ n m \le n mn )。

如果数组 c c c 中的元素可以重新排列,使得其中至少有 k k k 个元素与数组 b b b 中的元素匹配,那么 Maxim 认为长度为 m m m 的数组 c c c 是好数组。

例如,如果 b = [ 1 , 2 , 3 , 4 ] b = [1, 2, 3, 4] b=[1,2,3,4] k = 3 k = 3 k=3 ,那么数组 [ 4 , 1 , 2 , 3 ] [4, 1, 2, 3] [4,1,2,3] [ 2 , 3 , 4 , 5 ] [2, 3, 4, 5] [2,3,4,5] 就是好数组(它们可以按如下方式重新排列: [ 1 , 2 , 3 , 4 ] [1, 2, 3, 4] [1,2,3,4] [ 5 , 2 , 3 , 4 ] [5, 2, 3, 4] [5,2,3,4] ),而数组 [ 3 , 4 , 5 , 6 ] [3, 4, 5, 6] [3,4,5,6] [ 3 , 4 , 3 , 4 ] [3, 4, 3, 4] [3,4,3,4] 则不是好数组。

马克西姆希望选择长度为 m m m 的数组 a a a 的每个子段作为数组 c c c 的元素。请帮助 Maxim 计算有多少个被选中的数组是好的。

换句话说,求有多少个位置 1 ≤ l ≤ n − m + 1 1 \le l \le n - m + 1 1lnm+1 的元素 a l , a l + 1 , … , a l + m − 1 a_l, a_{l+1}, \dots, a_{l + m - 1} al,al+1,,al+m1 构成一个好数组。

思路:

朴素思想是直接枚举每个子段然后暴力验证,到这里时间复杂度为 O ( n m ) O(nm) O(nm),之后也不好验证有 k k k 个数相同。时间上肯定超了,因为 n = 2 ∗ 1 0 5 n=2*10^5 n=2105,所以我们需要至少 n l o g n nlogn nlogn 的做法。

考虑通过递推来得到每个字段的信息。具体来说,我们先暴力算出第一段 a 1 ∼ a m a_1\sim a_m a1am 的信息,然后向后推,前面加一个元素,后面删一个元素,得到第二段 a 2 ∼ a m + 1 a_2\sim a_{m+1} a2am+1,同理向后推。

而统计一个子段是否和 b b b 数组有 k k k 个相同数字,我们可以用桶来统计,用 t a i ta_i tai 表示 a a a 数组里等于 i i i 的数有几个,同理 t b tb tb 数组,然后相同的数的个数就是 m i n { t a i , t b i } min\{ta_i,tb_i\} min{tai,tbi}

两种想法合一下就得到了正解:我们预处理好 t b tb tb,然后用滑动窗口的思想来递推每个子段,维护这个子段的桶 t a ta ta 和重复的数字个数即可。

code:

#include <iostream>
#include <cstdio>
using namespace std;
const int maxn=2e5+5;
const int maxk=1e6+5;

int T,n,m,k;
int a[maxn],b[maxn],ta[maxk],tb[maxk];

int main(){
	cin>>T;
	while(T--){
		cin>>n>>m>>k;
		for(int i=1;i<=n;i++)cin>>a[i];
		for(int i=1;i<=m;i++)cin>>b[i],tb[b[i]]++;
		
		int cnt=0,ans=0;
		for(int i=1;i<m;i++){
			cnt-=min(ta[a[i]],tb[a[i]]);
			ta[a[i]]++;
			cnt+=min(ta[a[i]],tb[a[i]]);
//			cout<<"&"<<ta[a[i]]<<" "<<tb[a[i]]<<" "<<cnt<<endl;
		}
		
		cnt-=min(ta[a[m]],tb[a[m]]);
		ta[a[m]]++;
		cnt+=min(ta[a[m]],tb[a[m]]);
		if(cnt>=k)ans++;
		for(int i=m+1;i<=n;i++){
			cnt-=min(ta[a[i-m]],tb[a[i-m]]);
			ta[a[i-m]]--;
			cnt+=min(ta[a[i-m]],tb[a[i-m]]);
			cnt-=min(ta[a[i]],tb[a[i]]);
			ta[a[i]]++;
			cnt+=min(ta[a[i]],tb[a[i]]);
			
			if(cnt>=k)ans++;
		}
		cout<<ans<<endl;
		for(int i=n-m;i<=n;i++)ta[a[i]]=0;
		for(int i=1;i<=m;i++)tb[b[i]]=0;
	}
	return 0;
}

E. Long Inversions

题意:

给出长度为 n n n 的二进制字符串 s s s 。二进制字符串是仅由字符 "1 "和 "0 "组成的字符串。

您可以选择一个整数 k k k ( 1 ≤ k ≤ n 1 \le k \le n 1kn ),然后任意多次进行以下运算:选择字符串中连续的 k k k 个字符并将其反转,即用 "1 "替换所有 “0”,反之亦然。

通过这些操作,您需要使字符串中的所有字符都等于 “1”。

例如,如果是 n = 5 n=5 n=5 s = 00100 s=00100 s=00100 ,则可以选择 k = 3 k=3 k=3 并进行如下操作:

  • 选择从第 1 1 1 个到第 3 3 3 个字符的子串,得到 s = 110 00 s=\color{blue}{110}\color{black}00 s=11000
  • 选择从第 3 3 3 个到第 5 5 5 个字符的子串,得到 s = 11 111 s=\color{black}11\color{blue}{111} s=11111

求出 k k k 的最大值,在此值下,可以通过所述操作使字符串中的所有字符都等于 “1”。请注意,实现这一目标所需的操作次数并不重要。

思路:

因为 n ≤ 5000 n\le 5000 n5000,所以我们直接枚举 k k k,然后 O ( n ) O(n) O(n) 验证即可。

验证的时候枚举 每个位置,如果这个位置为 0 0 0,就以它为左端点进行区间翻转。最后的 k − 1 k-1 k1 个位置宽度不够,不够以它为左端点来翻转,所以我们对最后 k − 1 k-1 k1 个位置进行验证,如果正好能对上,那么这个 k k k 就是合法的,否则不合法。

区间翻转肯定不可能暴力来做,所以我们差分优化一下即可。

code:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
using namespace std;

int T,n;
string str;

int main(){
	cin>>T;
	while(T--){
		cin>>n>>str;
		vector<int> t(n+5);
		for(int i=1;i<=n;i++)
			t[i]=(str[i-1]=='1');
		
		for(int k=n;k>=1;k--){
			vector<int> a(n+5);
			bool f=true;
			
			for(int i=1;i+k-1<=n;i++){
				a[i]+=a[i-1];
				if((a[i]&1)==t[i]){
					a[i]+=1;
					a[i+k]-=1;
				}
			}
			for(int i=n-k+1+1;i<=n;i++){
				a[i]+=a[i-1];
				if((a[i]&1)==t[i]){
					f=false;
					break;
				}
			}
			if(f){
				cout<<k<<endl;
				break;
			}
		}
	}
	return 0;
}

F. Unfair Game

题意:

晚上,爱丽丝和鲍勃聚在一起,就一个由 n n n 个整数组成的数列玩了一个刺激的游戏,这个数列中的每个整数都不超过 4 4 4。游戏规则太复杂,无法描述,所以我们只描述获胜条件–如果序列中所有数字的 二进制异或 都非零,则爱丽丝获胜;否则,鲍勃获胜。

他们邀请夏娃担任裁判。一开始,爱丽丝和鲍勃用 n n n 个数字进行游戏。一局游戏结束后,夏娃从序列中移除一个数字,然后爱丽丝和鲍勃用 n − 1 n-1 n1 个数字进行游戏。夏娃再次删除一个数字,然后爱丽丝和鲍勃用 n − 2 n - 2 n2 个数字进行游戏。这个过程一直持续到数字序列为空为止。

夏娃似乎认为在这样的游戏中,爱丽丝几乎总是赢,所以她希望鲍勃赢的次数越多越好。如果夏娃以最佳方式移除数字,求鲍勃能赢爱丽丝的最大次数。

输入

第一行包含一个整数 t t t ( 1 ≤ t ≤ 1 0 4 1 \le t \le 10^4 1t104 ) - 测试用例的数量。

每个测试用例的第一行也是唯一一行包含四个整数 p i p_i pi ( 0 ≤ p i ≤ 200 0 \le p_i \le 200 0pi200 ) ,表示游戏开始时序列中 1、2、3 和 4 的个数。

思路:

题面看半天没看明白,就是告诉你 1 , 2 , 3 , 4 1,2,3,4 1,2,3,4 的个数,然后每次选一个数移除,剩下的数的异或和为 0 0 0 的时候会产生一次贡献,问最多会产生多少次贡献。

1,2,3 的二进制位之间会互相影响,但是 4 4 4 不会和其他数产生影响,先看 4 4 4

我们假设 1 , 2 , 3 1,2,3 1,2,3 的异或和为 0 0 0,只看 4 4 4。不难发现当 4 4 4 的个数为偶数的时候就会产生一次贡献,因此只有 4 4 4 能得到 ⌊ p 4 2 ⌋ \left\lfloor\dfrac {p_4} 2\right\rfloor 2p4 个贡献。而当我们去看 1 , 2 , 3 1,2,3 1,2,3 的时候,其实发现这几个数也是同理的,比如 1 , 2 , 4 1,2,4 1,2,4 的异或和为 0 0 0 的话, 3 3 3 就可以产生 ⌊ p 3 2 ⌋ \left\lfloor\dfrac {p_3} 2\right\rfloor 2p3 贡献,类推 1 , 2 1,2 1,2。这样依靠这种方式我们最多可以得到 ⌊ p 1 2 ⌋ + ⌊ p 2 2 ⌋ + ⌊ p 3 2 ⌋ + ⌊ p 4 2 ⌋ \left\lfloor\dfrac {p_1} 2\right\rfloor+\left\lfloor\dfrac {p_2} 2\right\rfloor+\left\lfloor\dfrac {p_3} 2\right\rfloor+\left\lfloor\dfrac {p_4} 2\right\rfloor 2p1+2p2+2p3+2p4 个贡献。

不同点在于,如果 4 4 4 的异或和为 0 0 0,并且 1 , 2 , 3 1,2,3 1,2,3 都剩余奇数个的时候,这时候也会产生一次贡献。假设 1 , 2 1,2 1,2 都有奇数个, 3 3 3 能产生的贡献就是 ⌊ p 3 − 1 2 ⌋ \left\lfloor\dfrac {p_3-1} 2\right\rfloor 2p31 个。因此这种情况算出来是 ⌊ p 1 − 1 2 ⌋ + ⌊ p 2 − 1 2 ⌋ + ⌊ p 3 − 1 2 ⌋ + ⌊ p 4 − 1 2 ⌋ \left\lfloor\dfrac {p_1-1} 2\right\rfloor+\left\lfloor\dfrac {p_2-1} 2\right\rfloor+\left\lfloor\dfrac {p_3-1} 2\right\rfloor+\left\lfloor\dfrac {p_4-1} 2\right\rfloor 2p11+2p21+2p31+2p41 个贡献。两种情况不会交叉出现。

综上,我们还是采用第一种方式来凑贡献,不过这样有个特殊情况,那就是一开始就是满足第二种情况( 4 4 4 的个数无所谓,减去一个对第一种情况统计的答案没有影响),这时会白给了一个贡献。

code:

#include <iostream>
#include <cstdio>
using namespace std;

int T,a,b,c,d;

int main(){
	cin>>T;
	while(T--){
		cin>>a>>b>>c>>d;
		int ans=a/2+b/2+c/2+d/2+(a%2 && b%2 && c%2);
		cout<<ans<<endl;
	}
	return 0;
}

G. GCD on a grid

题意:

不久前,Egor 学习了求两个数的最大公约数的欧几里得算法。 a a a b b b 这两个数的最大公约数是除以 a a a b b b 而不留余数的最大数。有了这些知识,Egor 可以解决他曾经无法解决的问题。

瓦西里有一个 n n n 行和 m m m 列的网格,整数 a i j {a_i}_j aij 位于 i i i 行和 j j j 列的交点上。Egor 想从左上角(第一行和第一列的交点)移动到右下角(最后一行和最后一列的交点),并找出路径上所有数字的最大公约数。他只能向下和向右移动。Egor 写下了几条路径,并得到了不同的 GCD 值。他开始对寻找最大可能的 GCD 感兴趣。

不幸的是,Egor 已经厌倦了计算 GCD,因此他请求您帮助他找出从网格左上角到右下角的路径上整数的最大 GCD。

思路:

看的这个大佬的,图也是摘的人家的。

有个结论,就是 1 0 6 10^6 106 以内的数的约数最多不会超过 240 240 240

在这里插入图片描述
再加上 n , m ≤ 100 n,m\le 100 n,m100,于是就不难想到设 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示到达位置 i , j i,j i,j 所有可能的公因数,递推就是暴力地枚举所有可能的公因数向下递推即可。dp值是个容器,存储所有可能的公因数,可以使用set来存储,这样的时间复杂度是 O ( n ∗ m ∗ 240 ∗ l o g 240 ) O(n*m*240*log240) O(nm240log240)

看着挺美好,但是这个题阴险的地方在于设定了 ∑ n ∗ m ≤ 2 ∗ 1 0 5 \sum n*m\le 2*10^5 nm2105,此时再用上面的方法就会超时( 2 ∗ 1 0 5 ∗ 240 ∗ l o g 240 ≈ 4 ∗ 1 0 8 2*10^5*240*log240\approx 4*10^8 2105240log2404108)。考虑优化掉这个 l o g log log

我们可以枚举 a 1 , 1 a_{1,1} a1,1 所有可能的约数(因为它是出发点,一定会经过它),然后去验证每一个约数,这样就不需要使用容器set来进行维护了,也就优化掉了 l o g log log

code:

没优化 l o g log log,TLE9

#include <iostream>
#include <cstdio>
#include <vector>
#include <set>
using namespace std;
const int maxn=105;
const int maxa=1e6+5;

int T,n,m;
int a[maxn][maxn];

int gcd(int a,int b){
	while(b)b^=a^=b^=a%=b;
	return a;
}


int main(){
	cin>>T;
	while(T--){
		cin>>n>>m;
		vector<vector<set<int> > > dp(n+1,vector<set<int> >(m+1));
		for(int i=1;i<=n;i++)
			for(int j=1;j<=m;j++)
				cin>>a[i][j];
		
		int t=gcd(a[1][1],a[n][m]);
		for(int i=1;i*i<=t;i++){
			if(t%i==0){
				dp[1][1].insert(i);
				dp[1][1].insert(t/i);
			}
		}
		for(int i=1;i<=n;i++)
			for(int j=1;j<=m;j++){
				if(i>1){
					for(auto x:dp[i-1][j])
						dp[i][j].insert(gcd(x,a[i][j]));
				}
				if(j>1){
					for(auto x:dp[i][j-1])
						dp[i][j].insert(gcd(x,a[i][j]));
				}
			}
		cout<<*dp[n][m].rbegin()<<endl;
	}
	return 0;
}

优化掉 l o g log log

#include <iostream>
#include <cstdio>
#include <vector>
#include <set>
using namespace std;
const int maxn=105;
const int maxa=1e6+5;
 
int T,n,m;
int a[maxn][maxn];
 
int gcd(int a,int b){
	while(b)b^=a^=b^=a%=b;
	return a;
}
 
bool vis[maxn][maxn];
bool check(int gd){
	vis[0][1]=true;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++){
			if(a[i][j]%gd==0)vis[i][j]=vis[i-1][j]|vis[i][j-1];
			else vis[i][j]=false;
		}
	return vis[n][m];
}
 
int main(){
	cin.tie(0)->sync_with_stdio(false);
	cin>>T;
	while(T--){
		cin>>n>>m;
		
		for(int i=1;i<=n;i++)
			for(int j=1;j<=m;j++)
				cin>>a[i][j];
		
		int t=gcd(a[1][1],a[n][m]);
		set<int> gd;
		for(int i=1;i*i<=t;i++){
			if(t%i==0){
				gd.insert(i);
				gd.insert(t/i);
			}
		}
		for(auto it=gd.rbegin();it!=gd.rend();it++)
			if(check(*it)){
				cout<<*it<<endl;
				break;
			}
	}
	return 0;
}

H. The Most Reckless Defense

题意:

您正在玩的是一款非常受欢迎的塔防游戏,名为 “Runnerfield 2”。在这款游戏中,玩家要设置防御塔,攻击从某个起点向玩家基地移动的敌人。

您会得到一个大小为 n × m n \times m n×m 的网格,网格上已经放置了 k k k 个塔楼,并铺设了一条敌人移动的路径。第 x x x 行和第 y y y 列的交叉点上的单元格表示为 ( x , y ) (x, y) (x,y)

每秒,防御塔会对其范围内的所有敌人造成 p i p_i pi 单位的伤害。例如,如果敌人位于 ( x , y ) (x, y) (x,y) 单元,而防御塔位于 ( x i , y i ) (x_i, y_i) (xi,yi) 单元,其攻击范围为 r r r ,那么敌人将在 ( x − x i ) 2 + ( y − y i ) 2 ≤ r 2 (x - x_i) ^ 2 + (y - y_i) ^ 2 \le r ^ 2 (xxi)2+(yyi)2r2 时受到 p i p_i pi 单位的伤害。

敌人会从 ( 1 , 1 ) (1, 1) (1,1) 单元移动到 ( n , m ) (n, m) (n,m) 单元,路径上的每个单元都会恰好经过一次。敌人会立即横向或纵向移动到相邻的单元格,但在移动之前,它会在当前单元格停留一秒。如果在这一秒内敌人的生命值为零或更低,则无法再移动。如果敌人到达 ( n , m ) (n, m) (n,m) 格并可以再移动一次,玩家就输了。

默认情况下,所有防御塔的攻击范围都为零,但玩家可以将防御塔的攻击范围设置为整数 r r r ( r < 0 r<0 r<0 ),在这种情况下,所有敌人的生命值都会增加 3 r 3^r 3r 。不过,每个 r r r 最多只可用于一座防御塔。

假设敌人的基础生命值为 h h h 个单位。如果防御塔的范围是 2 2 2 4 4 4 5 5 5 ,那么敌人在路径开始时的生命值将是 h + 3 2 + 3 4 + 3 5 = h + 9 + 81 + 243 = h + 333 h + 3 ^ 2 + 3 ^ 4 + 3 ^ 5 = h + 9 + 81 + 243 = h + 333 h+32+34+35=h+9+81+243=h+333 。范围的选择在敌人出现前进行一次,游戏开始后无法更改。

找出基础生命值 h h h 的最大值,在此范围内设置范围,当生命值为 h h h 的敌人通过时,玩家就不会输(不考虑塔范围的加成)。

思路:

我们给一个防御塔设定半径的时候,我们可以得到一个更广的攻击范围,然后攻击范围里有多少 #,我们就能造成几倍的 p i p_i pi 伤害,但是我们会付出敌人血量增加 3 r 3^r 3r 的代价。多个塔攻击范围重叠可以重复造成伤害,也就是说塔之间不会相互影响,只有半径不能重复一个限制条件。

比较容易想到的是,一座塔造成的总伤害如果不如它增加的血量的话,我们一定不会这么干,因为我们还不如一开始就不给它设定半径,还能省下一个半径给其他塔用。因此就需要满足攻击范围里的 # 个数 c n t cnt cnt 乘以 p i p_i pi 大于 3 r 3^r 3r。而 3 r 3^r 3r 增长非常快,可以预想到 r r r 根本用不了几个,粗略估计一下的话, c n t ∗ p i ≤ ( 2 ∗ r − 1 ) 2 ∗ 500 < 3 r cnt*p_i\le (2*r-1)^2*500\lt 3^r cntpi(2r1)2500<3r 可以算出到 r = 12 r=12 r=12 时就不满足条件了。

因为 r r r 很小,我们可以暴力枚举每个 r r r,然后对每个 r r r 暴力去算在范围中的 # 的个数,得到 c n t ∗ p i − 3 r cnt*p_i-3^r cntpi3r,也就是这座塔我们能收多少过路费。

因为塔与塔之间有个半径会相互影响,所以我们在处理第 i i i 座塔的时候,需要知道之前用了哪些半径,存储一下之前使用的半径,因为半径很小,我们可以直接状压。这样就得到了状压dp的做法。

d p [ i ] [ s t ] dp[i][st] dp[i][st] 表示前 i i i 座塔使用半径情况为 s t st st 时,敌人最大的基础生命值。递推方程就是 d p [ i ] [ s t ] = m a x { d p [ i − 1 ] [ s t ] , d p [ i ] [ s t   x o r   j ] + h } ( j ∈ s t , j 只有一个 1 ) ( h = c n t ∗ p i − 3 r ) dp[i][st]=max\{dp[i-1][st],dp[i][st\ xor\ j]+h\}\quad\\(j\in st,j只有一个1)(h=cnt*p_i-3^r) dp[i][st]=max{dp[i1][st],dp[i][st xor j]+h}(jst,j只有一个1)(h=cntpi3r)

总的运算次数为 k ∗ r ∗ ( n ∗ m + 2 r ) ≤ n ∗ m ∗ 12 ∗ ( n ∗ m + 2 12 ) ≤ 197 , 880 , 000 k*r*(n*m+2^r)\le n*m*12*(n*m+2^{12})\le 197,880,000 kr(nm+2r)nm12(nm+212)197,880,000,时限 3 3 3 s,不会超时。

发现递推式第 i i i 维只和第 i − 1 i-1 i1 维有关,可以滚动数组优化。

code:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <vector>
using namespace std;
typedef long long ll;
const int maxn=55;
const int inf=1e9;

int T,n,m,k;
string mp[maxn];
struct towers{
	int x,y,p;
}tow[maxn*maxn];

int qpow(int a,int b){
	int base=a,ans=1;
	while(b){
		if(b&1)ans*=base;
		base*=base;
		b>>=1;
	}
	return ans;
}
int calc(int x,int y,int r){//坐标为(x,y) 半径为r包含了多少'#'点 
	int cnt=0;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			if(mp[i][j]=='#' && (i-x)*(i-x)+(j-y)*(j-y)<=r*r)
				cnt++;
	return cnt;
}

int main(){
	cin.tie(0)->sync_with_stdio(false);
	
	cin>>T;
	while(T--){
		cin>>n>>m>>k;
		for(int i=1;i<=n;i++){
			cin>>mp[i];
			mp[i]=" "+mp[i];
		}
		for(int i=1,x,y,p;i<=k;i++){
			cin>>x>>y>>p;
			tow[i]={x,y,p};
		}
		
		//dp[i][st] 前i个防御塔 半径选择为st的最大基础生命值 
		vector<vector<ll> > dp(k+1,vector<ll>(1<<12,-inf));
		dp[0][0]=0;
		for(int i=1;i<=k;i++){
			auto [x,y,p]=tow[i];
			for(int r=1;r<=12;r++){
				int st=1<<(r-1),cnt=calc(x,y,r),h=cnt*p-qpow(3,r);
				
				for(int j=0;j<(1<<12);j++){
					dp[i][j]=max(dp[i][j],dp[i-1][j]);
					if(j&st)dp[i][j]=max(dp[i][j],dp[i-1][j^st]+h);
				}
			}
		}
		ll ans=0;
		for(int i=0;i<(1<<12);i++)
			ans=max(ans,dp[k][i]);
		cout<<ans<<endl;
	}
	return 0;
}

滚动数组优化,优化到 80kb 内存。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <vector>
using namespace std;
typedef long long ll;
const int maxn=55;
const int inf=1e9;

int T,n,m,k;
string mp[maxn];
struct towers{
	int x,y,p;
}tow[maxn*maxn];

int qpow(int a,int b){
	int base=a,ans=1;
	while(b){
		if(b&1)ans*=base;
		base*=base;
		b>>=1;
	}
	return ans;
}
int calc(int x,int y,int r){//坐标为(x,y) 半径为r包含了多少'#'点 
	int cnt=0;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			if(mp[i][j]=='#' && (i-x)*(i-x)+(j-y)*(j-y)<=r*r)
				cnt++;
	return cnt;
}

int main(){
	cin.tie(0)->sync_with_stdio(false);
	
	cin>>T;
	while(T--){
		cin>>n>>m>>k;
		for(int i=1;i<=n;i++){
			cin>>mp[i];
			mp[i]=" "+mp[i];
		}
		for(int i=1,x,y,p;i<=k;i++){
			cin>>x>>y>>p;
			tow[i]={x,y,p};
		}
		
		vector<vector<ll> > dp(2,vector<ll>(1<<12,-inf));
		dp[0][0]=0;
		for(int i=1;i<=k;i++){
			auto [x,y,p]=tow[i];
			auto &pre=dp[(i-1)&1],&t=dp[i&1];
			for(int j=0;j<(1<<12);j++)t[j]=-inf;
			//上面这句写不写无所谓
			//因为dp值是不断变大的,后面的dp值一定不会被前面的dp值覆盖掉
			
			for(int r=1;r<=12;r++){
				int st=1<<(r-1),cnt=calc(x,y,r),h=cnt*p-qpow(3,r);
				
				for(int j=0;j<(1<<12);j++){
					t[j]=max(pre[j],t[j]);
					if(j&st)t[j]=max(t[j],pre[j^st]+h);
				}
			}
		}
		ll ans=0;
		for(int i=0;i<(1<<12);i++)
			ans=max(ans,dp[k&1][i]);
		cout<<ans<<endl;
	}
	return 0;
}
  • 23
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值