想法
读完题就发现有一个贪心地方,每次升级小蓝必然选择当前收益最高的,也就是增加攻击力最多的技能,因此只需要枚举 m 此,用堆维护收益最高的技能就可以成功地 TLE 本题。
时间复杂度:O(mlogn)
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#define x first
#define y second
using namespace std;
const int N = 1e5 + 10;
typedef pair<int, int> PII;
priority_queue<PII> heap;
int a[N], b[N];
int main()
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i ++)
{
cin >> a[i] >> b[i];
heap.push({a[i], i});
}
int ans = 0;
for(int i = 1; i <= m; i ++)
{
ans += heap.top().x;
int id = heap.top().y;
heap.pop();
a[id] -= b[id];
heap.push({a[id], id});
}
cout << ans << endl;
return 0;
}
优化
接下来考虑优化。
沿着贪心的思路往下走,如果每次都选择收益最大的技能 i,我们将每次选择得到的攻击力 排成一个序列,这个序列一定是单调递减的。
以样例为例,最后选取技能所组成的序列为:
10,9,8,7,7,6
通过序列的单调性萌生了一个二分答案的想法——二分最后一次选择得到的攻击力 l 。
因此所有 >l 的攻击力都是可以被选择的,而 =l 的攻击力则需要看情况,check
函数自然就是判断选择次数是否 ≥m。
此时我们需要一个快速得到选择次数的方法,可以简单推导一下:
假设对于技能 i,我们选择了 t 次,则有:
ai−bi×(t−1)≥l ai−l≥bi×(t−1) ai−lbi≥t−1 ⌊ai−lbi⌋=t−1 t=⌊ai−lbi⌋+1
由此可在 O(1) 时间内套公式求出选择次数,有一些细节在代码中。
这是 bool check()
函数与二分的实现:
bool check(int x)
{
int sum = 0;
for(int i = 1; i <= n; i ++)
if(a[i] >= x) // 如果 < x 则可能会出现负数
sum += (a[i] - x) / b[i] + 1;
return sum >= m;
}
int l = 0, r = 1e6 + 5;
while(l <= r)
{
int mid = l + r >> 1;
if(check(mid)) l = mid + 1;
else r = mid - 1;
}
l --; // 这个二分板子需要 l--
现在我们已经在 O(nlogn) 时间内求出了最后一次(也是最小)的攻击力,只需要根据这个得到最终的答案就好了。
假设对于技能 i,我们选择了 t 次,我们同样可以在 O(1) 的时间内找出技能 i 对答案的总贡献
左为技能 i 对答案的总贡献,右为选择次数 t。
{a12a−b23a−3b34a−6b45a−10b5…ta−t(t−1)2bt
得到一个快速求技能 i 对答案的总贡献的公式。
for(int i = 1; i <= n; i ++)
{
int t = ((a[i] - l) / b[i] + 1); // 次数公式
if(t <= 0 || a[i] < l) continue; // 防止负数
if(a[i] - (t - 1) * b[i] == l) // 特殊处理刚好等于最后一次攻击力的情况
t --;
ans += t * a[i] - (t * (t - 1) / 2 * b[i]); // 贡献公式
m -= t; // 剩余可用次数减去此处已用次数
}
答案部分需要加上 m×l,表示等于最后一次选择攻击力 l 的技能的贡献
完整代码
时间复杂度:O(nlogn)
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#define int long long
using namespace std;
const int N = 1e5 + 10;
int a[N], b[N];
int n, m;
bool check(int x)
{
int sum = 0;
for(int i = 1; i <= n; i ++)
if(a[i] >= x)
sum += (a[i] - x) / b[i] + 1;
return sum >= m;
}
signed main()
{
int tot = 0;
cin >> n >> m;
for(int i = 1; i <= n; i ++)
{
cin >> a[i] >> b[i];
tot += (a[i] - 1) / b[i] + 1;
}
m = min(m, tot); // 防止可选择次数 > 能选择的次数出现负数
int ans = 0;
int l = 0, r = 1e6 + 5;
while(l <= r)
{
int mid = l + r >> 1;
if(check(mid)) l = mid + 1;
else r = mid - 1;
}
l --;
for(int i = 1; i <= n; i ++)
{
int t = ((a[i] - l) / b[i] + 1);
if(t <= 0 || a[i] < l) continue;
if(a[i] - (t - 1) * b[i] == l)
t --;
ans += t * a[i] - (t * (t - 1) / 2 * b[i]);
m -= t;
}
cout << ans + m * l << endl;
return 0;
}
感谢 atzy 大佬指出的错误!
写的最清楚的
转了一圈还是回来点了个赞 %%%%%
一下就看懂了 赞
多谢楼主!!写的真的棒!
请问二分里为什么是r = mid - 1而不是r = mid呢
为了避免 TLE,如果你改成
r = mid
会进入死循环谢谢
写的很赞
%%%%
%
感觉和楼主的逻辑已经完全一样了,不知为什么过不了
#include <iostream> using namespace std; const int N = 1e5 + 10; long long n, m; long long l; // 最后一次增加的攻击力 long long a[N], b[N]; bool check(long long mid) { int cnt = 0; for (int i = 1; i <= n; ++i) if (a[i] >= mid) cnt += (a[i] - mid) / b[i] + 1; return cnt >= m; } int main() { cin >> n >> m; long long tot = 0; for (int i = 1; i <= n; ++i) { cin >> a[i] >> b[i]; tot += (a[i] - 1) / b[i] + 1; } m = min(m, tot); long long l = 0, r = 1e6+5; while (l <= r) { long long mid = l + r >> 1; if (check(mid)) l = mid + 1; else r = mid - 1; } l--; long long sum = 0, res = 0; for (long long i = 1; i <= n; ++i) { long long t = (a[i] - l) / b[i] + 1; if (t <= 0 || a[i] < l) continue; if (a[i] - (t - 1) *b[i] == l) --t; res += t * a[i] - (t * (t - 1) / 2 * b[i]); m -= t; } cout << res + m * l << endl; return 0; }
define int long long
就好了确实好了,排查了一下是check函数里面的int cnt爆了…
淦,#define int long long,因为这个调了好久qaq
暴力只能过一半
oler好想法
大佬
为什么最后要加上这个m*l嘞
明白了
妙啊妙啊
sum += (a[i] - x) / b[i] + 1; 这个地方为什么 是 a[i] -x?? x是指 等差数列 最后一项吗? 最后一项 不一定是 x, 有可能 比 x大,比如 最后一项可能是 x+100,或者其它
不是很懂你等差序列的说法,不过这个公式我上面推导过了
此处x是二分出来的mid,指的是所有选择中最后一次选择的攻击力,而不是指这个技能的
嗯,知道了,就是求 每个序列 大于等于x的 项数
😘
楼主 计算答案的for循环里面
if(a[i] - t * b[i] == l) t --;
应该是if(A[i] - (t - 1) * 1ll * B[i] == l) t -- ;
噢噢噢确实是,感谢指出问题
感谢楼主的题解!
写的很棒
谢谢!hh
ORZ
ORZ
6666