「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}\)
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;
}
posted @ 2020-09-19 15:06  Luckyblock  阅读(117)  评论(0编辑  收藏  举报