「CTSC2018」混合果汁
知识点: 二分答案,主席树
学习整体二分的时候屯的题,yy 了个在线的 check
发现总复杂度是两个 log 的= =
果断抛弃整体二分(
简述
给定 \(n\) 个物品,物品 \(i\) 的数量为 \(l_i\),单个花费为 \(p_i\),价值为 \(d_i\)。
对于一个物品的选择方案,定义其总花费为所有物品的花费之和,其价值为方案中物品价值的最小值。
给定 \(m\) 个询问,每次询问给定参数 \(g,L\),求一个物品的选择方案,使得方案中物品数 \(\ge L\),花费 \(\le g\),且价值最大,输出最大的价值。
\(1\le n,m\le 10^5\),\(1\le d_i, p_i, l_i\le 10^5\),\(1\le g, L\le 10^{18}\)。
2S,512MB。
分析
显然对于每个询问,答案满足单调性,考虑二分 选择方案中价值最小的物品 \(mid\)。 问题变为判定仅使用价值 \(\ge d_{mid}\) 的物品,总花费 \(\le g\) 时,能否选择 \(\ge L\) 个物品。
先考虑如何暴力 Check
。二分答案之后,所有 可选物品 贡献均变为 1。 为满足花费限制,贪心的想,肯定先选花费小的。
则可将可选物品按花费升序排序,从小到大选择物品,直至不能再选,判断选择的数量是否 \(\ge L\) 即可。
单次 Check
复杂度 \(O(n\log n + n)\),总复杂度 \(O(mn\log^2 n)\),期望得分 \(45\text{pts}\)。
发现每次 Check
的过程中,我们仅关心某花费的物品的数量。考虑权值线段树维护对应权值区间内 物品的个数,与全部选择它们时的总花费。
每次 Check
时先将所有可选物品插入线段树中,再线段树上二分判断是否存在合法的方案。特别的,二分到叶节点后,注意特判叶节点选择的数量,因为叶节点对应的物品花费最高,可能不能全部选择。
单次 Check
复杂度变为 \(O(n\log n + \log n)\),总复杂度仍为 \(O(mn\log^2 n)\)。
发现上述过程的瓶颈在于,每次 Check
的 \(mid\) 不同导致可选物品都不同。必须每次重新构建 价值 \(\ge d_{mid}\) 的物品组成的权值线段树。
考虑可持久化,先将物品按 \(d_i\) 排序后 插入主席树中,Check
时直接取出对应部分即可,避免了重建线段树。
单次 Check
复杂度变为 \(O(\log^2 n)\),总复杂度 \(O(m\log^2 n)\),期望得分 \(100\text{pts}\)。
由于每次询问的 \(g,L\le 10^{18}\),保证了不会乘爆,注意开 long long
。
代码实现
//知识点:二分答案,主席树
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstring>
#define ll long long
const int kMaxn = 1e5 + 10;
//=============================================================
struct Juice {
int d, p, l;
} a[kMaxn];
int n, m, maxp, ans, d[kMaxn], root[kMaxn];
ll g, L;
//=============================================================
inline ll read() {
ll f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3ll) + (w << 1ll) + (ch ^ '0');
return f * w;
}
bool CompareJuice(Juice fir, Juice sec) {
return fir.d < sec.d;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
namespace Hjt {
#define ls lson[now_]
#define rs rson[now_]
#define mid ((L_+R_)>>1)
int node_num, lson[kMaxn << 5], rson[kMaxn << 5];
ll cnt[kMaxn << 5], val[kMaxn << 5];
void Insert(int &now_, int pre_, int L_, int R_, int pos_, int cnt_) {
now_ = ++ node_num;
lson[now_] = lson[pre_];
rson[now_] = rson[pre_];
cnt[now_] = cnt[pre_] + 1ll * cnt_;
val[now_] = val[pre_] + 1ll * pos_ * cnt_;
if (L_ == R_) return ;
if (pos_ <= mid) Insert(ls, lson[pre_], L_, mid, pos_, cnt_);
else Insert(rs, rson[pre_], mid + 1, R_, pos_, cnt_);
}
ll Query(int lnow_, int rnow_, int L_, int R_, ll k_) {
if (L_ == R_) {
return std :: min((1ll * k_ / L_), cnt[rnow_] - cnt[lnow_]);
}
ll vall = val[lson[rnow_]] - val[lson[lnow_]];
ll cntl = cnt[lson[rnow_]] - cnt[lson[lnow_]];
if (vall < k_) return Query(rson[lnow_], rson[rnow_], mid + 1, R_, k_ - vall) + cntl;
return Query(lson[lnow_], lson[rnow_], L_, mid, k_);
}
#undef mid
}
bool Check(int pos_) {
return Hjt :: Query(root[pos_ - 1], root[n], 1, maxp, g) >= L;
}
//=============================================================
int main() {
n = (int) read(), m = (int) read();
for (int i = 1; i <= n; ++ i) {
a[i] = (Juice) {(int) read(), (int) read(), (int) read()};
Chkmax(maxp, a[i].p);
d[i] = a[i].d;
}
std :: sort(a + 1, a + n + 1, CompareJuice);
for (int i = 1; i <= n; ++ i) {
Hjt :: Insert(root[i], root[i - 1], 1, maxp, a[i].p, a[i].l);
}
while (m --) {
g = read(), L = read(), ans = - 1;
for (int l = 1, r = n; l <= r; ) {
int mid = (l + r) >> 1;
if (Check(mid)) {
ans = a[mid].d;
l = mid + 1;
} else {
r = mid - 1;
}
}
printf("%d\n", ans);
}
return 0;
}