loj 2721 [NOI2018] 屠龙勇士
loj 2721 [NOI2018] 屠龙勇士
有 \(n\) 条恶龙,每条恶龙有一个初始生命值 \(a_i\) ,恢复力 \(p_i\) , 击杀后会掉落一把剑.
初始你有 \(m\) 把剑.
给出每把剑的攻击力.
现在你打算按照以下方法杀掉所有恶龙
- 选择一个参数 \(x\)
- 从 \(1\) 到 \(n\) 开始杀龙,对于第 \(i\) 条龙
- 选择攻击力小于等于 \(a_i\) 的剑中攻击力最高的那把,如果不存在则选择所有剑里攻击力最小的一把,设其攻击力为 \(ATK\)
- 用这把剑攻击 \(x\) 次,然后这把剑消失,恶龙的生命值减少 \(ATK \cdot x\)
- 然后恶龙开始恢复,每次生命值增加 \(p_i\) ,如果某个时刻,恶龙的生命值为 \(0\) ,则击杀成功
问最小的 \(x\) ,如果不存在能杀掉所有恶龙的 \(x\) ,输出 \(-1\)
有 \(T\) 组数据
\(n \le 10^5, m \le 10^5, T \le 5, a_i \le 10^{12}\)
所有 \(p_i\) 的最小公倍数 \(\le 10^{12}\)
所有剑的攻击力 \(\le 10^6\)
Tutorial
很容易用multiset在 \(O(n \log n)\) 的时间得到每条龙会使用哪一把剑.特判了 \(m=0\) 的情况
设当前剑的攻击力为 \(b\) ,那么 \(x\) 需要满足
\[a_i - bx + kp_i = 0, k \ge 0
\]
其中 \(k \ge 0\) 的部分可以在最后强制 \(x \ge \lceil \dfrac {a_i} b \rceil\) 就好了.
现在可以写作
\[bx + kp_i = a_i
\]
这是一个一元二次方程,由于有 \(n\) 个这样的方程需要合并,所以我们将它转化为关于 \(x\) 的同余方程的形式
具体步骤就是,设 \(d = \gcd(b, p_i)\) ,若 \(d \nmid a_i\) 则一元二次方程无解.否则将 \(b,p_i,a_i\) 同时除以 \(d\) .然后就可以转化为
\[x \equiv a_i \cdot \dfrac 1b \mod p_i
\]
其中 \(b\) 和 \(p_i\) 此时是互质的,用exgcd计算逆元即可.
然后将这 \(n\) 个方程用扩展中国剩余定理合并.时间复杂度 \(O(n \log n)\)
由于 \(p_i\) 的最小公倍数是 \(10^{12}\) 级别的,所以需要快速乘.
总时间复杂度为 \(O(n \log n)\)
Code
#include <cstdio>
#include <iostream>
#include <set>
#define debug(...) fprintf(stderr,__VA_ARGS__)
using namespace std;
inline char nc()
{
static char buf[100000],*l=buf,*r=buf;
return l==r&&(r=(l=buf)+fread(buf,1,100000,stdin),l==r)?EOF:*l++;
}
template<class T> void read(T &x) {
x=0; int f=1,ch=nc();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=nc();}
while(ch>='0'&&ch<='9'){x=x*10-'0'+ch;ch=nc();}
x*=f;
}
template<class T> inline bool Cmax(T &x, T y) {return x < y ? x = y, 1 : 0;}
typedef long long ll;
typedef long double ld;
const int maxn = 1e5 + 50;
int T;
int n, m;
int c[maxn];
ll a[maxn], p[maxn];
multiset<ll> s;
inline ll mul(ll x, ll y, ll mod)
{
if(mod <= 1e9) return x * y % mod;
if(mod <= 1e12) return (((x * (y >> 20) % mod) << 20) + x * (y & ((1 << 20) - 1))) % mod;
ll re = x * y - (ll)((ld)x * y / mod + 0.5) * mod;
if(re < 0) re += mod;
return re;
}
ll gcd(ll a, ll b) {return b == 0 ? a : gcd(b, a % b);}
ll exgcd(ll a, ll b, ll &x, ll &y)
{
if(b == 0) {x = 1, y = 0; return a;}
ll d = exgcd(b, a % b, y, x);
y -= a / b * x;
return d;
}
inline ll inver(ll a, ll p)
{
ll x, y, d = exgcd(a, p, x, y);
x = (x % p + p) % p;
return x;
}
bool exCRT(ll &m0, ll &a0, ll m1, ll a1)
{
// debug("%lld %lld %lld %lld\n", m0, a0, m1, a1);
ll t = a1 - a0;
ll x, y, d = exgcd(m0, m1, x, y);
if(t % d) return 0;
ll M = m0 / d * m1;
m1 /= d, t /= d;
t = (t % M + M) % M;
x = mul((x % m1 + m1) % m1, t, M);
a0 = (mul(x, m0, M) + a0) % M;
m0 = M;
return 1;
}
ll solve()
{
if(m == 0) return -1;
ll mn = 0;
ll M = 1, A = 0;
for(int i = 1; i <= n; ++i)
{
multiset<ll>::iterator it = s.upper_bound(a[i]);
if(it != s.begin()) --it;
int b = *it;
s.erase(it);
Cmax(mn, (a[i] + b - 1) / b);
ll d = gcd(b, p[i]);
if(a[i] % d) return -1;
b /= d, p[i] /= d, a[i] /= d;
a[i] = mul(a[i], inver(b, p[i]), p[i]);
if(!exCRT(M, A, p[i], a[i])) return -1;
s.insert(c[i]);
}
if(A < mn) A += (mn - A + M - 1) / M * M;
else if(A > mn) A -= (A - mn) / M * M;
return A;
}
int main()
{
freopen("dragon.in", "r", stdin);
freopen("dragon.out", "w", stdout);
read(T);
for(int kase = 1; kase <= T; ++kase)
{
read(n), read(m);
for(int i = 1; i <= n; ++i) read(a[i]);
for(int i = 1; i <= n; ++i) read(p[i]);
for(int i = 1; i <= n; ++i) read(c[i]);
s.clear();
for(int i = 1; i <= m; ++i)
{
int x; read(x);
s.insert(x);
}
printf("%lld\n", solve());
}
return 0;
}
Summary
学会了一元二次方程转同余方程的方法.