1. 程式人生 > 實用技巧 >「CTSC2018」混合果汁

「CTSC2018」混合果汁

知識點: 二分答案,主席樹

原題面:Loj Luogu


學習整體二分的時候屯的題,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

複雜度 \(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


爆零小技巧

兩個 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
*/