Official

O - プレイリストの最大スコア / Maximum Score of a Playlist Editorial by kyopro_friends


末尾が楽曲 \(i\) であるプレイリストの総合スコアの最大値を \(\mathrm{dp}[i]\) とします。

末尾が楽曲 \(j\) で総合スコアが \(S\) であるプレイリストの末尾に楽曲 \(i\) を追加すると、そのプレイリストの総合スコアは \(S+A_i+B_jB_i\) となります。

よって、末尾が楽曲 \(i\) であるプレイリストの総合スコアの最大値は、\(i\) の前の楽曲が何であるかを考えることで

\(\begin{aligned} \mathrm{dp}[i] &=\max(A_i, \max_{j<i}(\mathrm{dp}[j]+A_i+B_jB_i))\\ &=A_i+\max(0, \max_{j<i}(\mathrm{dp}[j]+B_jB_i)) \end{aligned}\)

となります。ここで max を取る対象は、\(\mathrm{dp}[j]+B_jx\) という 1 次式の \(x=B_i\) のときの値であり、\(i\) の昇順にこのDPを行うとき、max を取る対象となる1次式の集合は単調に増大することから、CHT(convex hull trick)を用いて高速化することができ、 \(O(N\log N)\) でこの問題を解くことができます。

CHT

CHTとは、次のような、直線群に対する操作を行うクエリを、クエリあたり \(O(\log N)\) で処理するアルゴリズムのことです。

  • 直線 \(L\) が与えられる。直線の集合 \(S\) に \(L\) を追加する
  • 値 \(t\) が与えられる。直線の集合 \(S\) に属する直線たちの \(x=t\) での値の最大値を求める。

このアルゴリズムは、クエリで与えられる \(t\) が事前にわかっている場合には、セグメントツリーを模した二分木のデータ構造である Li Chao Tree を用いて実現することができます。

実装例

実装例 (C++)

#include <bits/stdc++.h>
using namespace std;
using ll=long long;

struct LiChaoTree{
  struct Line{
    ll a, b;
    Line(ll a, ll b): a(a), b(b){}
    ll eval(ll x){return a * x + b;}
  };

  int n;
  vector<ll> xs;
  vector<Line> seg;
  
  LiChaoTree(vector<ll> xs): xs(xs){
    n = xs.size();
    seg = vector<Line>(4 * n, Line(0, -1e18));
  }

  void add(ll a, ll b){
    Line line = Line(a, b);
    int i = 1, l = 0, r = n;

    while(r - l > 1){
      int m = (l + r) / 2;
      bool left = line.eval(xs[l]) > seg[i].eval(xs[l]);
      bool mid  = line.eval(xs[m]) > seg[i].eval(xs[m]);

      // midで強い方をこのノードに残す
      if(mid){
        swap(line, seg[i]);
      }
    
      //left, mid間で交差してるなら左にいく、そうでないなら右
      i *= 2;
      if(left != mid) r = m;
      else l = m, i++;
    }
    
    // 葉
    if(line.eval(xs[l]) > seg[i].eval(xs[l])){
      swap(line, seg[i]);
    }
  }
  
  ll query(ll x){
    int i = 1, l = 0, r = n;
    ll ans = seg[i].eval(x);

    while(r - l > 1){
      int m = (l + r) / 2;
      i *= 2;
      if(x < xs[m]) r = m;
      else l = m, i++;
      ans = max(ans, seg[i].eval(x));
    }
    return ans;
  }
};

int main(){
  int n;
  cin >> n;
  vector<int> a(n), b(n);
  for(int i=0; i<n; i++) cin >> a[i] >> b[i];

  set<int> s(b.begin(),b.end());
  vector<ll> bi(s.begin(),s.end());
  auto seg = LiChaoTree(bi);

  vector<ll> dp(n);
  dp[0] = a[0];
  seg.add(b[0], dp[0]);
  for(int i=1; i<n; i++){
    dp[i] = a[i] + max(0LL, seg.query(b[i]));
    seg.add(b[i], dp[i]);
  }

  cout << *max_element(dp.begin(), dp.end()) << endl;
}

実装例 (Python)

class LiChaoTree:
  class Line:
    def __init__(self, a, b):
      self.a = a
      self.b = b
    def eval(self, x):
      return self.a * x + self.b

  def __init__(self, xs):
    self.xs = xs
    self.n = len(xs)
    self.seg = [self.Line(0, -10**18) for _ in range(4 * self.n)]
  
  def add(self, a, b):
    line = self.Line(a, b)
    i = 1
    l, r = 0, self.n

    while r - l > 1:
      m = (l + r) // 2
      left = line.eval(self.xs[l]) > self.seg[i].eval(self.xs[l])
      mid  = line.eval(self.xs[m]) > self.seg[i].eval(self.xs[m])
      
      # mid で強い方をこのノードに残す
      if mid:
        line, self.seg[i] = self.seg[i], line
      
      # left, mid間で交差しているなら左に行く、そうでないなら右
      if left != mid:
        r = m
        i = 2 * i
      else:
        l = m
        i = 2 * i + 1

    # 葉
    if line.eval(self.xs[l]) > self.seg[i].eval(self.xs[l]):
      line, self.seg[i] = self.seg[i], line
  
  def query(self, x):
    i = 1
    l, r = 0, self.n
    ans = self.seg[i].eval(x)
    
    while r - l > 1:
      m = (l + r) // 2
      if x < self.xs[m]:
        r = m
        i = 2 * i
      else:
        l = m
        i = 2 * i + 1
      ans = max(ans, self.seg[i].eval(x))

    return ans

N = int(input())
AB = [tuple(map(int,input().split())) for _ in range(N)]

bi = sorted(set(b for _, b in AB))
seg = LiChaoTree(bi)

dp = [0] * N
a, b = AB[0]
dp[0] = a
seg.add(b, dp[0])

for i in range(1, N):
  a, b = AB[i]
  dp[i] = a + max(0, seg.query(b))
  seg.add(b, dp[i])

print(max(dp))

posted:
last update: