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 ceil) 就好了.
现在可以写作
[bx + kp_i = a_i
]
这是一个一元二次方程,由于有 (n) 个这样的方程需要合并,所以我们将它转化为关于 (x) 的同余方程的形式
具体步骤就是,设 (d = gcd(b, p_i)) ,若 (d mid 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
", 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
", solve());
}
return 0;
}
Summary
学会了一元二次方程转同余方程的方法.