本题做题思路参考 @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 ai⩽pi(性质1),那么就很好办了,我们可以很轻易地写出等式:
x ∗ u s e i ≡ a i ( m o d    p i ) x*use_i\equiv a_i(mod\:\:p_i) x∗usei≡ai(modpi)
也就是
x ∗ u s e i − y ∗ p i = a i x*use_i-y*p_i=a_i x∗usei−y∗pi=ai
其中 u s e i use_i usei表示当前使用的剑的攻击力.
但是并不是啊……
然而,如果我们仔细阅读题目,会发现那些不保证 a i ⩽ p i a_i\leqslant p_i ai⩽pi的测试点,都有另外一个性质:
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=1n⌈useiai⌉即可,加个特判就完事.
那么我们返回正题,看看 x ∗ u s e i ≡ a i ( m o d    p i ) x*use_i\equiv a_i(mod\:\:p_i) x∗usei≡ai(modpi)怎么处理.
遗憾的是,我们不能直接使用拓展中国剩余定理来求解该方程. 直接把 u s e i use_i usei莽到右边也是不现实的,因为 u s e i use_i usei在 m o d    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    p i ) x*use_i\equiv a_i(mod\:\:p_i) x∗usei≡ai(modpi)转换为 x ≡ a ( m o d    b ) x\equiv a (mod\:\:b) x≡a(modb)的形式.
不妨先解不定方程 x ∗ u s e i − y ∗ p i = a i x*use_i-y*p_i=a_i x∗usei−y∗pi=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 x∗usei−y∗pi=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+k∗gcd(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    m o d i ) x\equiv sol_i(mod\:\:mod_i) x≡soli(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) X≡0(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. 对于特判情况的考虑还不够.