loj 2721 [NOI2018] 屠龙勇士

loj 2721 [NOI2018] 屠龙勇士

https://loj.ac/problem/2721

\(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

学会了一元二次方程转同余方程的方法.

posted @ 2020-08-13 19:33  LJZ_C  阅读(99)  评论(0编辑  收藏  举报