「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}\)。
分析題意
顯然對於每個詢問,答案滿足單調性,考慮二分 選擇方案中價值最小的物品 \(mid\)。
問題變為判定僅使用價值 \(\ge d_{mid}\) 的物品,總花費 \(\le g\) 時,能否選擇 \(\ge L\) 個物品。
先考慮如何暴力 check
。
二分答案之後,所有 可選物品 貢獻均變為 1。
為滿足花費限制,貪心的想,肯定先選花費小的。
則可將可選物品按花費升序排序,從小到大選擇物品,直至不能再選,判斷選擇的數量是否 \(\ge L\) 即可。
單次 check
發現每次 check
的過程中,我們僅關心某花費的物品的數量。
考慮權值線段樹維護對應權值區間內 物品的個數,與全部選擇它們時的總花費。
每次 check
時先將所有可選物品插入線段樹中,再線段樹上二分判斷是否存在合法的方案。
特別的,二分到葉節點後,注意特判葉節點選擇的數量,因為葉節點對應的物品花費最高,可能不能全部選擇。
單次 check
複雜度變為 \(O(n\log n + \log n)\),總複雜度仍為 \(O(mn\log^2 n)\)。
發現上述過程的瓶頸在於,每次 check
必須每次重新構建 價值 \(\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
。
爆零小技巧
兩個 int
變數相乘後,得到的值寄存時仍為 int
型。
注意可能會乘爆,所有乘法都應轉化為 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;
}
/*
3 1
1 3 5
2 1 3
3 2 5
6 3
3 1
1 1 1
2 100000 1
3 1 2
2 2
*/