Opis
BOI 2016
Bosses
Park
Spiral
Cities
Maze
Swap
CEOI 2017
Skierowanie dróg
Pewny zakład
Pułapka Mariusza
Budujemy mosty
Pościg Mariusza
Palindromiczny podział
BOI 2018
Miłosny wielokąt
Marsjańskie DNA
Problemy dżdżownicy
Prąd przemienny
Genetyka
Ścieżki
BOI 2019
Flash Memory
Nautilus
Alpine Valley
Tom's Kitchen
Necklace
Olympiads
BOI 2020
Kolory
Mieszanki
Joker
Graf
Wioska
Wirusy
CEOI 2020
Fajny płot
Drogi
Star Trek
Napój Wielkiej Potęgi
Wiosenne porządki
Szachowa gorączka

Budujemy mosty

W zadaniu mamy wybudować most, wybierając do tego niektóre spośród n gotowych filarów. Pierwszy i ostatni filar mają być wybrane. Koszt zbudowania przęsła mostu między filarami i oraz j to (hihj)2. Koszt wyburzenia filaru i to wi. Zminimalizować koszt budowy.

Rozwiązanie kwadratowe

Naturalnym pomysłem na rozwiązanie zadania jest zastosowanie metody programowania dynamicznego. Będziemy wypełniać tabelkę, gdzie dp[i] oznacza koszt budowy mostu kończącego się i-tym filarem.

Dla uproszczenia najpierw zabezpieczymy pieniądze na wyburzenie wszystkich filarów w wysokości W=iwi. Dzięki temu będziemy mogli uwzględniać koszt niewyburzenia filaru równy wi.

Zatem dp[1]=w1, bo mamy wtedy most składający się z pierwszego filaru. Dla i>1 mamy prostą zależność rekurencyjną: dp[i]=minj<i(dp[j]+(hihj)2wi).

Odpowiedzią jest oczywiście dp[n]+W, a całość można zapisać następującym programem działającym w czasie O(n2):

const int N = 110000;
int n,h[N],w[N];
ll dp[N],sumw;

ll sq(int x) { return ll(x)*x; }

int main() {
  scanf("%d",&n);
  REP(i,n) scanf("%d",&h[i]);
  REP(i,n) scanf("%d",&w[i]);

  REP(i,n) sumw += w[i];

  dp[0] = -w[0];
  for (int i=1; i<n; ++i) {
    dp[i] = ll(1e18);
    for (int j=i-1; j>=0; --j) {
      dp[i] = min(dp[i], dp[j] + sq(h[i] - h[j]) - w[i]);
    }
  }

  ll ans = dp[n-1] + sumw;
  printf("%lld\n", ans);
}

Rozwiązanie wzorcowe

Możemy trochę inaczej rozpisać sobie koszt budowy. Przęsło mostu między filarami i oraz j kosztuje (hihj)2=hi22hihj+hj2. Składniki hi2 będą występować w sumarycznym koszcie dwukrotnie dla każdego wybranego filaru (oprócz pierwszego i ostatniego, dla których będą występować jednokrotnie). Zatem możemy zmienić koszt budowy przęsła na 2hi22hihj, a następnie całkowity koszt to będzie Ww1+h12hn2 plus koszt wynikający z budowy przęseł.

Niech dp[i] będzie kosztem budowy mostu kończącego się i-tym filarem. Wtedy dp[i]=minj<i(dp[j]wi+2hi22hihj)=minj<i(aj(2hi)+bj+ci), gdzie aj=dp[j] oraz bj są pewnymi stałymi dla j-tego filaru, a ci=wi+2hi jest stałą dla i-tego filaru.

Tak więc potrzebujemy umieć minimalizować wartości funkcji liniowych ajx+bj dla zbioru filarów, oraz dodać taką funkcję aix+bi dla nowego filaru.

Poniżej kod, który nieefektywnie realizuje taką strukturę w czasie O(n2):

struct oracle {
  vector<pair<ll,ll>> v;

  ll get(ll x) {
    ll ans = 1e18;
    for (auto [a, b] : v) {
      ans = min(ans, a*x + b);
    }
    return ans;
  }

  void insert(ll a, ll b) {
    v.emplace_back(a, b);
  }
};

int main() {
  scanf("%d",&n);
  REP(i,n) scanf("%d",&h[i]);
  REP(i,n) scanf("%d",&w[i]);

  REP(i,n) sumw += w[i];

  oracle s;
  s.insert(h[0], sumw - w[0] + sq(h[0]) - sq(h[n-1]));
  for (int i=1; i<n; ++i) {
    cost = -w[i] + 2*sq(h[i]) + s.get(-2*h[i]);
    s.insert(h[i], cost);
  }

  printf("%lld\n", cost);
}

Ale strukturę danych można zaimplementować, aby operacje działały w zamortyzowanym czasie O(logn). Jest to tzw. convex hull optimization, gdzie utrzymujemy górną otoczkę wypukłą zbioru równań liniowych. Poniżej przykładowa implementacja tego pomysłu:

const ll infty = 1e18;

inline ll ceil_div(ll a, ll b) {
  assert(b > 0);
  if (a < 0) return -( (-a)/b );
  else return (a + b-1) / b;
}

struct line {
  ll a, b;
  mutable ll xl;

  bool operator<(const line& other) const {
    if (max(a, other.a) == infty+1) {
      return xl < other.xl;
    } else {
      return make_pair(a, b) < make_pair(other.a, other.b);
    }
  }

  ll inters(const line& prev) const {
    return ceil_div(prev.b - b, a - prev.a);
  }

  bool valid(const line& prev, const line& next) const {
    return inters(prev) < next.inters(*this);
  }

  void update(const line& prev) const {
    xl = inters(prev);
  }
};

struct envelope {
  set<line> s;

  void insert(ll a, ll b) {
    assert(max(abs(a), abs(b)) <= infty);

    auto it = s.insert({ a, b, -2*infty }).first;

    if (next(it) != s.end() && a == next(it)->a) {
      s.erase(it);
      return;
    }
    if (it != s.begin() && prev(it)->a == a) {
      s.erase(prev(it));
    }

    if (it != s.begin() && next(it) != s.end() && !it->valid(*prev(it), *next(it))) {
      s.erase(it);
      return;
    }

    if (it != s.begin()) {
      auto pit = prev(it);
      while (pit != s.begin() && !pit->valid(*prev(pit), *it)) {
        s.erase(pit);
        pit = prev(it);
      }
      it->update(*pit);
    }

    if (next(it) != s.end()) {
      auto nit = next(it);
      while (next(nit) != s.end() && !nit->valid(*it, *next(nit))) {
        s.erase(nit);
        nit = next(it);
      }
      nit->update(*it);
    }
  }

  bool empty() const { return s.empty(); }
  void clear() { s.clear(); }

  pair<ll,ll> eval(ll x) const {
    auto it = prev(s.upper_bound({ infty+1, 0, x }));
    return make_pair(it->a, it->b);
  }
};

struct oracle {
  envelope env;

  ll get(ll x) {
    auto [a, b] = env.eval(x);
    return -a*x - b;
  }

  void insert(ll a, ll b) {
    env.insert(-a, -b);
  }
};