P4774 [NOI2018]屠龙勇士(exCRT,multiset)

7 篇文章 0 订阅
3 篇文章 0 订阅

P4774 [NOI2018]屠龙勇士

本题做题思路参考 @shadowice1984的题解

简要题意

n n n条巨龙和 m m m把剑,每条巨龙有 a i a_i ai的初始生命值,有 p i p_i pi的恢复能力. 一条巨龙被杀死当且仅当其受到的伤害使其剩余血量恰好为 0 0 0或者在任意次恢复后恰好为 0 0 0.

而每次拿来杀巨龙的剑也有要求:

1. 1 . 1. 优先选择当前拥有的,攻击力不高于巨龙初始生命值中攻击力最大的一把剑作为武器.

2. 2 . 2.如果没有这样的剑,则选择当前攻击力最低的一把剑作为武器.

每杀死一条巨龙,用于杀死这条巨龙的剑就会报销,但同时也会掉落新的一把攻击力为 a t x i atx_i atxi的剑.

攻击次数 x x x是一定的. 如果全部巨龙都可以按以上的规则杀死,那么输出满足以上条件的最小的 x x x,否则输出 − 1 -1 1.

题目解析

由于巨龙的血量是固定的,新剑得到的顺序也是固定的,所以实际上剑的使用顺序也是固定的. 我们只需要一种数据结构维护剑的使用顺序就好了,这种数据结构必须满足:

有序,可以方便地查找不大于一个数的最大值,可以方便地插入和删除一个数.

考虑到 s e t set set会吞掉重复的数字,所以我们使用多重集合 m u l t i s e t multiset multiset. 按照上面的要求,我们要在 m u l t i s e t multiset multiset中使用的函数有以下几个:

upper_bound,insert,erase

代码实现单独挑出来看如下:

#include<bits/stdc++.h>
using namespace std;
int n,m,a[100];
multiset<int >s;
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		s.insert(a[i]);
	}
	multiset<int >::iterator it;
	it=((m<*s.begin())?s.begin():--s.upper_bound(m));
	s.insert(m+1);
	s.erase(it);
	while(!s.empty()){
		cout<<*s.begin()<<endl;
		it=s.begin();
		s.erase(it); 
	}
	s.clear();
	return 0;
}
注释1:begin、upper_bound返回的值都是指针,所以数值要加上*得到.
注释2:注意迭代器it的使用方法

想要了解更多关于 m u l t i s e t multiset multiset的知识,请点击这里.



现在我们已经解决了什么时候哪条龙被哪把剑砍的问题,预处理完毕,接下来的就是求 x x x了.

由题目描述,我们知道,每条巨龙有 a i a_i ai的初始生命值,有 p i p_i pi的恢复能力. 一条巨龙被杀死当且仅当其受到的伤害使其剩余血量恰好为 0 0 0或者在任意次恢复后恰好为 0 0 0.

好像我又复诵了一遍,不管了

假设 a i ⩽ p i a_i\leqslant p_i aipi(性质1),那么就很好办了,我们可以很轻易地写出等式:

x ∗ u s e i ≡ a i ( m o d &MediumSpace;&MediumSpace; p i ) x*use_i\equiv a_i(mod\:\:p_i) xuseiai(modpi)

也就是

x ∗ u s e i − y ∗ p i = a i x*use_i-y*p_i=a_i xuseiypi=ai

其中 u s e i use_i usei表示当前使用的剑的攻击力.

但是并不是啊……

然而,如果我们仔细阅读题目,会发现那些不保证 a i ⩽ p i a_i\leqslant p_i aipi的测试点,都有另外一个性质:

p i = 1 p_i=1 pi=1

这种情况非常简单, x = m a x i = 1 n ⌈ a i u s e i ⌉ x=max^n_{i=1}\left \lceil \frac{a_i}{use_i} \right \rceil x=maxi=1nuseiai即可,加个特判就完事.

那么我们返回正题,看看 x ∗ u s e i ≡ a i ( m o d &MediumSpace;&MediumSpace; p i ) x*use_i\equiv a_i(mod\:\:p_i) xuseiai(modpi)怎么处理.

遗憾的是,我们不能直接使用拓展中国剩余定理来求解该方程. 直接把 u s e i use_i usei莽到右边也是不现实的,因为 u s e i use_i usei m o d &MediumSpace;&MediumSpace; p i mod\:\:p_i modpi意义下不一定存在逆元.

ax=1(mod n), x称为a的逆元.
一个数在模另一个数的意义下有逆元的充要条件是gcd(a,n)=1, 此时逆元唯一存在.

我们现在要做的就是尝试把 x ∗ u s e i ≡ a i ( m o d &MediumSpace;&MediumSpace; p i ) x*use_i\equiv a_i(mod\:\:p_i) xuseiai(modpi)转换为 x ≡ a ( m o d &MediumSpace;&MediumSpace; b ) x\equiv a (mod\:\:b) xa(modb)的形式.

不妨先解不定方程 x ∗ u s e i − y ∗ p i = a i x*use_i-y*p_i=a_i xuseiypi=ai,得到一个最小的非负整数解 x = s o l i x=sol_i x=soli.

那么不定方程 x ∗ u s e i − y ∗ p i = a i x*use_i-y*p_i=a_i xuseiypi=ai的所有解就可以表示成 x = s o l i + k ∗ p i g c d ( p i , u s e i ) x=sol_i+k*\frac{p_i}{gcd(p_i,use_i)} x=soli+kgcd(pi,usei)pi

m o d i = p i g c d ( p i , u s e i ) mod_i=\frac{p_i}{gcd(p_i,use_i)} modi=gcd(pi,usei)pi,以上等式两边同时对于 m o d i mod_i modi取模,得到

x ≡ s o l i ( m o d &MediumSpace;&MediumSpace; m o d i ) x\equiv sol_i(mod\:\:mod_i) xsoli(modmodi)

接下来用拓展中国剩余定理求出以上同余方程组最小的通解即可.



你以为完了吗?并没有!

理论上来说,还有特例存在.

1. 1. 1.如果所有的 a i = p i a_{i}=p_{i} ai=pi​的话我们解同余方程的结果将会是 0 0 0,但是事实上这是不对的.

直接求出杀死每条龙所需的刀数然后所有刀数求一个 l c m lcm lcm即可.

2. 2. 2.我们的 u s e i use_{i} usei​是 p i p_{i} pi​的倍数时一般情况时杀不掉这条龙的. 但是呢,如果 a i = p i a_{i}=p_{i} ai=pi​就很有趣了,此时这个方程等于没用,需要把它扔掉,换成一个没啥用的方程,比如 X ≡ 0 ( m o d 1 ) X\equiv 0(mod 1) X0(mod1)就行了.

好像是数据太水了导致没有加特判也能A

程序实现

#include<bits/stdc++.h>
#define ll long long
#define maxn 100010
using namespace std;
int n,m;
multiset<ll >s;
ll xi,yi;
ll sol[maxn],mod[maxn],use[maxn],a[maxn],p[maxn],atx[maxn],str[maxn];
ll fast_mul(ll ai,ll bi,ll md){
	ll ret=0;
	while(bi){
		if(bi&1)ret=(ret+ai)%md;
		ai=(ai+ai)%md;
		bi>>=1;
	}
	return ret;
}//龟速乘
ll exgcd(ll ai,ll bi){
	if(bi==0){
		xi=1,yi=0;
		return ai;
	}
	ll ret=exgcd(bi,ai%bi);
	ll zi=xi;
	xi=yi;
	yi=zi-(ai/bi)*yi;
	return ret;
}//exgcd
ll excrt(){
	ll ans=sol[1],N=mod[1];
	for(int i=2;i<=n;i++){
		ll ai=N,bi=mod[i],ci=(sol[i]-ans%bi+bi)%bi;
		ll gcd=exgcd(ai,bi),bg=bi/gcd;
		if(ci%gcd!=0){return -1;}
		xi=fast_mul(xi,ci/gcd,bg);
		ans+=xi*N;
		N*=bg;
		ans=(ans%N+N)%N;
	}
	return (ans%N+N)%N;
}//excrt
bool operate(){
	for(int i=1;i<=n;i++){
		ll ai=use[i],bi=p[i],ci=a[i];
		ll gcd=exgcd(ai,bi),bg=bi/gcd;
		if(ci%gcd!=0){return 0;}
		xi=fast_mul(xi,ci/gcd,bg);
		xi=(xi%bi+bi)%bi;//必须保证xi是一个最小的正整数
		sol[i]=xi;
		mod[i]=bg;//第一轮操作,把原式转化为可以使用excrt的形式
	}
	return 1;
}
inline void another_operate(){
	ll maxm=0;
	for(int i=1;i<=n;i++){
		ll op=((a[i]%use[i]==0)?a[i]/use[i]:a[i]/use[i]+1);//别忘了向上取整只限于小数位
		maxm=max(op,maxm);
	}
	printf("%lld\n",maxm);
}
inline void solve(){
	scanf("%d%d",&n,&m);
	bool check=false;
	for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
	for(int i=1;i<=n;i++){scanf("%lld",&p[i]);if(a[i]>p[i])check=true;}//特判
	for(int i=1;i<=n;i++)scanf("%lld",&atx[i]);
	for(int i=1;i<=m;i++){
		scanf("%lld",&str[i]);
		s.insert(str[i]);
	}
	multiset<ll >::iterator it;
	for(int i=1;i<=n;i++){
		it=((a[i]<*s.begin())?s.begin():--s.upper_bound(a[i]));
		use[i]=*it;
		s.insert(atx[i]);
		s.erase(it);
	}//multiset预处理每次使用的剑
	if(check){another_operate();return;};//发现不满足性质1
	bool flag=operate();
	if(!flag){printf("-1\n");return;}
	printf("%lld\n",excrt());
}
inline void clear(){s.clear();}//别忘了清空multiset
int main(){
	int T;
	scanf("%d",&T);
	for(int i=1;i<=T;i++){
		solve();
		clear();
	}
	return 0;
}

反思总结

1. 1. 1. 说实话我还是第一次认真做这种有大量的子任务的题,所以不太习惯,像一开始的分类讨论我就没能想到,也没能注意到不同取值对应的特点.

2. 2. 2. S T L STL STL不够熟练,导致想用又不会用,浪费了大量的时间.

3. 3. 3. 读题还是不够细心,题目中黑体标注的恰好为0我很久才审出来.

4. 4. 4. 面对数论题,有一点畏难心理,推导总是畏手畏脚的,不过随着做题数的增加逐渐好转了.

5. 5. 5. 对于特判情况的考虑还不够.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值