【Luogu P3994】高速公路

Problem

Background

\(C\) 国拥有一张四通八达的高速公路树,其中有 \(n\) 个城市,城市之间由一共 \(n-1\) 条高速公路连接。除了首都 \(1\) 号城市,每个城市都有一家本地的客运公司,可以发车前往全国各地,有若干条高速公路连向其他城市,这是一个树型结构,\(1\) 号城市(首都)为根。假设有一个人要从 \(i\) 号城市坐车出发前往 \(j\) 号城市,那么他要花费 \(P_i\)*(\(i\) 城市到 \(j\) 城市的距离)\(+Q_i\) 元。由于距离首都越远,国家的监管就越松,所以距离首都越远,客运公司的 \(P_i\)(单位距离价格)越大,形式化的说,如果把高速路网看成一棵以首都为根的有根树,\(i\) 号城市是 \(j\) 号城市的某个祖先,那么一定存在 \(P_i\le P_j\)

Description

大宁成为了国家统计局的调查人员,他需要对现在的高速路网进行一次调查,了解从其他每一个城市到达首都1号城市所花费的金钱(路径必须是简单路径)。

因为有非常多转车(或不转车)的抵达首都的方法,所以人工计算这个结果是十分复杂的。大宁非常的懒,所以请你编写一个程序解决它。

Input Format

\(1\) 行包含 \(1\) 个非负整数 \(n\),表示城市的个数。

\(2\)\(n\) 行,每行描述一个除首都之外的城市。其中第 \(i\) 行包含 \(4\) 个非负整数 \(F_i,S_i,P_i,Q_i\),分别表示 \(i\) 号城市的父亲城市,它到父亲城市高速公路的长度,以及乘车价格的两个参数。

Output Format

输出包含 \(n-1\) 行,每行包含一个整数。

其中第 \(i\) 行表示从 \(i+1\) 号城市 出发,到达首都最少的乘车费用。

Sample

Input 1

6
1 9 3 0
1 17 1 9
1 1 1 6
4 13 2 15
4 9 2 4

Output 1

27
26
7
43
24

Input 2 and Output 2

百度网盘

Range

对于前 \(40\%\) 的数据 \(1\le n\le1000\)

对于另外 \(20\%\) 的数据 满足从第 \(i\)\(i\neq1\))个城市出发的高速公路连向第 \(i-1\) 个城市。

对于所有的数据 \(1\le n\le1000000,0\le P_i,Q_i\le2^{31}–1\),保证结果不会大于 \(2^{63}–1\)

Algorithm

\(DP\),斜率优化

Mentality

看看这个题目,其实一下子就能想到我们可以在每一条从根节点到叶子节点的路径上线性 \(DP\)

那么方程也异常简单:设 \(f[i]\) 为从 \(i\) 点到达根节点所需最小费用,\(pre_i\)\(i\) 到根节点路径上所有点的集合,\(lj[i]\)\(i\) 到根节点的距离,那么自然有:
\[ f[i]=Min_{j\in pre_i}(f[j]+(lj[i]-lj[j])*P[i]+Q[i]) \]
当然,这个 \(DP\) 方程的复杂度是不过关的。按照一贯的套路,这种很容易想式子的 \(DP\) 题总满足决策单调性。所以我们尝试一下列出它的决策之间有什么关系。

\(i\) 的两个决策点为 \(j,k\) ,其中 \(j\in pre_k\)\(j,k\in pre_i\)

如果从点 \(k\) 转移比从点 \(j\) 转移更优,那么自然有:
\[ f[k]+(lj[i]-lj[k])*P[i]+Q[i]<f[j]+(lj[i]-lj[j])*P[i]+Q[i]\\ f[k]-lj[k]*P[i]<f[j]-lj[j]*P[i]\\ f[k]-f[j]<(lj[k]-lj[j])*P[i]\\ \frac{f[k]-f[j]}{lj[k]-lj[j]}<P[i] \]
我们发现,由于题目有 \(P_i>P_{pre_i}\) 的条件,那么当我们在一条链上 \(DP\) 时,一旦从某一点开始满足上式,则这个点的子树内所有点一直都会满足上式。换句话说,对于这个点的子树内所有点而言,\(k\) 转移总会比从 \(j\) 转移更优

换而言之,此题满足决策单调性中的斜率优化的条件。

那么由于 \(P[i]\) 递增,我们只需要维护一个斜率单调递增的决策点队列即可。

但是这是一棵树,怎么办?

很简单,每个点在更改决策点的单调队列时只有三个操作:

  • 不断 \(++head\) ,直到队首斜率不满足上式
  • 不断 \(--tail\) ,直到对尾斜率符合单调性
  • \(++tail\) ,在队列尾部插入当前决策点

那么我们发现,每到一个新的点,其实我们只是修改了一下 \(head\)\(tail\) 的值,并修改了队列中的一个元素。则我们只需要记住修改之前的那个元素的值,修改之前的队首对尾的位置即可,当我们退出当前点的时候,只需要照着之前记录的值改回去就好了!

但是对于这题还不够。

一般而言,我们的斜率优化总会是 \(O(n)\) 的,因为每个点最多只会被入队出队一次,所以总体复杂度只会取决于决策点个数而不是枚举决策点。

但是在这题里,一个决策点可能在这个点里入队,在下一个点里出队,而回溯回来的时候又再次入队,因而不符合每个点最多只会入队出队一次的规则!,换句话说,这样做的话,复杂度是错误的。

那接下来又怎么做呢?

由于我们的决策点队列里,相邻两点之间的斜率是单调的,所以我们完全可以通过二分来找到应该 \(++head,--tail\) 所达到的位置!

只需要找到最左边的,不满足斜率不等式的位置,以及最右边的,不满足斜率单调递增的位置即可。

当然,二分可能有点小细节,可以看看我的代码。(良心注释)

Code

#include <cstdio>
#include <iostream>
using namespace std;
int n;
int cntr, head[1000001], to[1000001], nx[1000001], w[1000001];
int Q[1000001];
int que[1000001], h, t;
long long P[1000001], lj[1000001], f[1000001];
void addr(int u, int v, int W) {
  cntr++;
  to[cntr] = v, nx[cntr] = head[u], w[cntr] = W;
  head[u] = cntr;
}
double slope(int a, int b) {
  return (double)(f[b] - f[a]) / (lj[b] - lj[a]);
}  //斜率不等式
void DP(int x) {
  int p, tph = h, tpt = t, tpw, l = h + 1, r = t,
         ans = h;  // l 的初值为 h+1,因为我们用来比较的是 mid-1 与 mid
                   // 两个位置的斜率。之所以不用 l=h,r=t-1 作为初值并比较 mid 与
                   // mid+1 ,是因为根据我们做斜率题的时候 h
                   // 总会在最右边的符合不等式的位置的右一个。这样二分出來的位置刚刚好;ans
                   // 的初值为 h,因为如果找不到相应的位置,那就不需要修改 h。
  while (l <= r) {
    int mid = (l + r) >> 1;
    if (slope(que[mid - 1], que[mid]) <= P[x])
      l = mid + 1, ans = mid;
    else
      r = mid - 1;
  }
  h = ans, p = que[h];
  f[x] = f[p] + (lj[x] - lj[p]) * P[x] + Q[x];
  l = h, r = t - 1, ans = t;  //这里的理由与队首差不多,都是为了结果正确性
  while (l <= r) {
    int mid = (l + r) >> 1;
    if (slope(que[mid], que[mid + 1]) >= slope(que[mid + 1], x))
      r = mid - 1, ans = mid;
    else
      l = mid + 1;
  }
  t = ans;
  tpw = que[++t], que[t] = x;  //记录即将被更改的位置的元素并更新
  for (int i = head[x]; i; i = nx[i]) {
    lj[to[i]] = lj[x] + w[i];  //更新路径长度
    DP(to[i]);
  }
  que[t] = tpw;
  h = tph, t = tpt;  //修改回进入这个点之前的状态
}
int main() {
  freopen("3994.in", "r", stdin);
  freopen("3994.out", "w", stdout);
  cin >> n;
  int F, W;
  for (int i = 2; i <= n; i++) {
    scanf("%d%d%lld%d", &F, &W, &P[i], &Q[i]);
    addr(F, i, W);
  }
  t = -1;  //最开始队列是空的,所以 tail=head-1
  DP(1);
  for (int i = 2; i <= n; i++) printf("%lld\n", f[i]);
}
posted @ 2019-05-20 20:12 洛水·锦依卫 阅读(...) 评论(...) 编辑 收藏