传送门
思路
唉,我太弱了,什么都不会,斜率优化也不会,唉,我太弱啦!
显然可以
DP,这样就得到了 30 分。考虑斜率优化。设我们正在计算
,可能的决策点
比可能的决策点
更深。若
比
更优,我们有:
化简得:
由于 并不单调,因此我们需要在凸壳上二分。但显然我的知识水平是不够的,因为题目还有一个 的限制。下面我们来逐一解决……
别人家的题解
唉,我太弱了,别人的题解一句话就把这道题切了,我写两页都切不了,别人写的我又看不懂,根本没有学过那些黑科技,唉,我太弱啦!
别人家的题解写道:方法 1:线段树维护可持久化单调队列;方法 2:点分治
QAQ 全都不会啊。
不如我们先看一下这道题的弱化版。
弱化的传送门(Luogu 3994 高速公路)
哪里弱化了:没有距离限制 ,且 是递增的。
做了弱化版的题有助于做这道题吗?当然不可能 QAQ
我们仍然有斜率优化的不等式:
由于 是递增的,因此,如果这棵树是一条链,那么这就是一个裸的单调队列斜率优化 DP。现在在树上,可以怎么做呢?
我们考虑可持久化。当然,这个可持久化是概念上的,我们还没有规定要用什么具体的数据结构。我们考虑直接用数组维护这个单调队列。观察发现,这个单调队列实际上可以看成一个单调栈(因为我们没有向队头插入元素,所以队头那一部分的数据是不受影响的),所以我们只需要考虑如何维护队尾就好了。当我们遍历到一个结点上时,我们会弹出一些元素,然后入队一个元素。弹出元素的操作可以通过修改队尾下标来实现,也就是说队列中的数据实际上还在。但是入队一个元素会导致一个数据被覆盖,我们记录一下这个被覆盖的数据是什么就好了。当我们需要得到父结点对应的版本时,我们恢复被覆盖的那个数据,将队头和队尾的下标还原即可。
但是这样做每个元素的出队次数不能使用均摊分析。考虑二分出队位置,这样我们就能在 的时间复杂度内解决这个问题了。
参考代码
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <cstring>
#include <cassert>
#include <cctype>
#include <climits>
#include <ctime>
#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <map>
#include <set>
#include <bitset>
#include <list>
#include <functional>
typedef long long LL;
typedef unsigned long long ULL;
using std::cin;
using std::cout;
using std::endl;
typedef LL INT_PUT;
INT_PUT readIn()
{
INT_PUT a = 0; bool positive = true;
char ch = getchar();
while (!(ch == '-' || std::isdigit(ch))) ch = getchar();
if (ch == '-') { positive = false; ch = getchar(); }
while (std::isdigit(ch)) { a = a * 10 - (ch - '0'); ch = getchar(); }
return positive ? -a : a;
}
void printOut(INT_PUT x)
{
char buffer[20]; int length = 0;
if (x < 0) putchar('-'); else x = -x;
do buffer[length++] = -(x % 10) + '0'; while (x /= 10);
do putchar(buffer[--length]); while (length);
putchar('\n');
}
const int maxn = int(1e6) + 5;
int n;
int parent[maxn];
LL cost[maxn];
LL p[maxn], q[maxn];
struct Graph
{
struct Edge
{
int to;
LL cost;
int next;
} edges[maxn];
int i;
int head[maxn];
Graph() : i() { memset(head, -1, sizeof(head)); }
void addEdge(int from, int to, LL cost)
{
edges[i].to = to;
edges[i].cost = cost;
edges[i].next = head[from];
head[from] = i;
i++;
}
#define idx(G) idx_##G
#define wander(G, node) for (int idx(G) = G.head[node]; ~idx(G); idx(G) = G.edges[idx(G)].next)
#define DEF(G) const Graph::Edge& e = G.edges[idx(G)]; int to = e.to; LL cost = e.cost
} G;
LL f[maxn];
LL depth[maxn];
int deque[maxn];
int head, tail;
double slope(int j, int k)
{
return (double)(f[j] - f[k]) / (depth[j] - depth[k]);
}
LL DP(int i, int j)
{
return f[j] + (depth[i] - depth[j]) * p[i] + q[i];
}
void DFS(int node)
{
wander(G, node)
{
DEF(G);
depth[to] = depth[node] + cost;
int preHead = head; // 保存之前的队头和队尾
int preTail = tail;
if (tail - head > 1)
{
int k = 0;
while (1 << k < (tail - head - 1)) k++;
for (int i = k; ~i; i--) if (head + (1 << i) < tail)
{
if (slope(deque[head + (1 << i)],
deque[head + (1 << i) - 1]) < p[to])
{
head += 1 << i;
}
}
}
f[to] = DP(to, deque[head]);
if (tail - head > 1)
{
int k = 0;
while (1 << k < (tail - head - 1)) k++;
for (int i = k; ~i; i--) if (head + (1 << i) < tail)
{
if (slope(to, deque[tail - (1 << i)]) <
slope(deque[tail - (1 << i)], deque[tail - (1 << i) - 1]))
{
tail -= 1 << i;
}
}
}
int pos = tail; // 保存被覆盖的数据
int val = deque[pos];
deque[tail++] = to;
DFS(to);
head = preHead; // 还原队头队尾以及被覆盖的数据
tail = preTail;
deque[pos] = val;
}
}
void run()
{
n = readIn();
for (int i = 2; i <= n; i++)
{
parent[i] = readIn();
cost[i] = readIn();
p[i] = readIn();
q[i] = readIn();
G.addEdge(parent[i], i, cost[i]);
}
deque[tail++] = 1;
DFS(1);
for (int i = 2; i <= n; i++)
printOut(f[i]);
}
int main()
{
run();
return 0;
}
对于没有距离限制的 50 分
我们可以将以上例题的单调队列改成单调栈。因为上面的单调队列实质上是一个可以忽略部分栈底内容的栈,所以要得到这 50 分还是很容易的。由于我从来没有写过这种单调栈类型的斜率优化,所以还是贴一份代码吧……
参考代码
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <cstring>
#include <cassert>
#include <cctype>
#include <climits>
#include <ctime>
#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <map>
#include <set>
#include <bitset>
#include <list>
#include <functional>
typedef long long LL;
typedef unsigned long long ULL;
using std::cin;
using std::cout;
using std::endl;
typedef LL INT_PUT;
INT_PUT readIn()
{
INT_PUT a = 0; bool positive = true;
char ch = getchar();
while (!(ch == '-' || std::isdigit(ch))) ch = getchar();
if (ch == '-') { positive = false; ch = getchar(); }
while (std::isdigit(ch)) { a = a * 10 - (ch - '0'); ch = getchar(); }
return positive ? -a : a;
}
void printOut(INT_PUT x)
{
char buffer[20]; int length = 0;
if (x < 0) putchar('-'); else x = -x;
do buffer[length++] = -(x % 10) + '0'; while (x /= 10);
do putchar(buffer[--length]); while (length);
putchar('\n');
}
const int maxn = int(1e6) + 5;
int n;
int parent[maxn];
LL cost[maxn];
LL p[maxn], q[maxn];
struct Graph
{
struct Edge
{
int to;
LL cost;
int next;
} edges[maxn];
int i;
int head[maxn];
Graph() : i() { memset(head, -1, sizeof(head)); }
void addEdge(int from, int to, LL cost)
{
edges[i].to = to;
edges[i].cost = cost;
edges[i].next = head[from];
head[from] = i;
i++;
}
#define idx(G) idx_##G
#define wander(G, node) for (int idx(G) = G.head[node]; ~idx(G); idx(G) = G.edges[idx(G)].next)
#define DEF(G) const Graph::Edge& e = G.edges[idx(G)]; int to = e.to; LL cost = e.cost
} G;
LL f[maxn];
LL depth[maxn];
int stack[maxn];
int tail;
double slope(int j, int k)
{
return (double)(f[j] - f[k]) / (depth[j] - depth[k]);
}
LL DP(int i, int j)
{
return f[j] + (depth[i] - depth[j]) * p[i] + q[i];
}
void DFS(int node)
{
wander(G, node)
{
DEF(G);
depth[to] = depth[node] + cost;
int preTail = tail;
int k = 0;
while (1 << k < (tail - 1)) k++;
int cnt = 0;
for (int i = k; ~i; i--) if (cnt + (1 << i) < tail)
{
if (slope(stack[cnt + (1 << i)],
stack[cnt + (1 << i) - 1]) < p[to]) // note
{
cnt += 1 << i;
}
}
f[to] = DP(to, stack[cnt]);
if (tail > 1)
{
for (int i = k; ~i; i--) if ((1 << i) < tail)
{
if (slope(to, stack[tail - (1 << i)]) <
slope(stack[tail - (1 << i)], stack[tail - (1 << i) - 1]))
{
tail -= 1 << i;
}
}
}
int pos = tail;
int val = stack[pos];
stack[tail++] = to;
DFS(to);
tail = preTail;
stack[pos] = val;
}
}
void run()
{
n = readIn();
if (readIn() > 1) throw; // 只能处理没有距离限制的情况
for (int i = 2; i <= n; i++)
{
parent[i] = readIn();
cost[i] = readIn();
p[i] = readIn();
q[i] = readIn();
readIn(); // l
G.addEdge(parent[i], i, cost[i]);
}
stack[tail++] = 1;
DFS(1);
for (int i = 2; i <= n; i++)
printOut(f[i]);
}
int main()
{
run();
return 0;
}
需要注意的是,虽然在单调栈上的斜率优化可以理解成找到凸包的切线,但是如果这样想的话边界情况就很难理解。最好的理解方法是:抛弃所有斜率小于目标斜率的直线,即抛弃所有满足不等式的点,这样,我们的最终结果就是一个明确的点,而不会在边界情况出现歧义了。
对于 100 分的数据
先了解一下为什么不能将 50 分做法扩展到 100 分做法来:
我们考虑这么一个做法:还是用可持久化单调栈(弱化版题目用的那个),但是为当前链上的每一个元素都开一个单调栈。假设最远只能到达 ,那么我们找到 对应的那个单调栈,在上面二分,这样正确性就有了保证。
我们来分析一下时间复杂度。修改单个单调栈的时间复杂度是 的(不要忘了不能使用均摊分析),总共有 个单调栈需要修改,因此整个算法的时间复杂度是 。
考虑优化,这里我就直接给出如何优化的,我们用线段树结构进行优化。具体地说,我们维护一棵线段树,线段树上的每个结点都对应一个单调栈,那么空间复杂度显然就是 的。设线段树上的结点的左端点为 ,每个单调栈保存的内容为深度大于等于 的结点对应的单调栈。每次修改时,我们修改所有覆盖了待修改深度的结点,总共有 个,每个要花费 的时间去修改。查询时,我们查询可以到达的结点深度对应的区间,总共要查 次,每个要花费 的时间。深搜结束时,我们要花费 的时间复杂度还原。所以总时间复杂度为 。
为什么可以像这样分成多个单调栈来查询呢?原因其实很简单。我们使用单调栈的目的是排除无用决策,显然的是,最终答案对应的决策点一定不会是无用决策,所以它在任何一个单调栈中都不会被排除。因此,最优决策点一定在 个单调栈中的一个里。
参考代码
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <cstring>
#include <cassert>
#include <cctype>
#include <climits>
#include <ctime>
#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <map>
#include <set>
#include <bitset>
#include <list>
#include <functional>
typedef long long LL;
typedef unsigned long long ULL;
using std::cin;
using std::cout;
using std::endl;
typedef LL INT_PUT;
INT_PUT readIn()
{
INT_PUT a = 0; bool positive = true;
char ch = getchar();
while (!(ch == '-' || std::isdigit(ch))) ch = getchar();
if (ch == '-') { positive = false; ch = getchar(); }
while (std::isdigit(ch)) { a = a * 10 - (ch - '0'); ch = getchar(); }
return positive ? -a : a;
}
void printOut(INT_PUT x)
{
char buffer[20]; int length = 0;
if (x < 0) putchar('-'); else x = -x;
do buffer[length++] = -(x % 10) + '0'; while (x /= 10);
do putchar(buffer[--length]); while (length);
putchar('\n');
}
const int maxn = int(2e5) + 5;
int n;
int logn;
int parent[19][maxn];
LL cost[maxn];
LL p[maxn], q[maxn], l[maxn];
struct Graph
{
struct Edge
{
int to;
LL cost;
int next;
} edges[maxn];
int i;
int head[maxn];
Graph() : i() { memset(head, -1, sizeof(head)); }
void addEdge(int from, int to, LL cost)
{
edges[i].to = to;
edges[i].cost = cost;
edges[i].next = head[from];
head[from] = i;
i++;
}
#define idx(G) idx_##G
#define wander(G, node) for (int idx(G) = G.head[node]; ~idx(G); idx(G) = G.edges[idx(G)].next)
#define DEF(G) const Graph::Edge& e = G.edges[idx(G)]; int to = e.to; LL cost = e.cost
} G;
LL f[maxn];
int depth[maxn];
LL dis[19][maxn];
LL Depth[maxn];
int stack[maxn];
int tail;
double slope(int j, int k)
{
return (double)(f[j] - f[k]) / (Depth[j] - Depth[k]);
}
LL DP(int i, int j)
{
return f[j] + (Depth[i] - Depth[j]) * p[i] + q[i];
}
class SegTree
{
int stack[maxn * 19];
static inline int code(int l, int r)
{
return (l + r) | (l != r);
}
int size;
int head[maxn * 2];
int tail[maxn * 2];
void build(int l, int r)
{
head[code(l, r)] = tail[code(l, r)] = size;
size += r - l + 1;
if (l == r)
return;
int mid = (l + r) >> 1;
build(l, mid);
build(mid + 1, r);
}
int g_Pos, g_Val;
int g_L, g_R;
struct RestoreRecord
{
int stackCode;
int preTail;
int prePos;
int preVal;
RestoreRecord() {}
RestoreRecord(int sc, int pt, int pp, int pv) :
stackCode(sc), preTail(pt), prePos(pp), preVal(pv) {}
};
std::vector<std::vector<RestoreRecord> > rr;
std::vector<RestoreRecord> curRecord;
void modify(int l, int r)
{
const int& h = head[code(l, r)];
int& t = tail[code(l, r)];
int step = 1;
int cnt = t;
if (t - h > 1)
{
while (step < t - h) step <<= 1;
for (; step; step >>= 1) if (h + step < cnt)
{
if (slope(g_Val, stack[cnt - step]) <
slope(stack[cnt - step], stack[cnt - step - 1]))
{
cnt -= step;
}
}
}
curRecord.push_back(RestoreRecord(code(l, r), t, cnt, stack[cnt]));
t = cnt;
stack[t++] = g_Val;
if (l == r)
return;
int mid = (l + r) >> 1;
if (g_Pos <= mid) modify(l, mid);
if (g_Pos > mid) modify(mid + 1, r);
}
LL query_(int l, int r) const
{
if (g_L <= l && r <= g_R)
{
const int& h = head[code(l, r)];
const int& t = tail[code(l, r)];
int step = 1;
while (step < t - h) step <<= 1;
int cnt = h;
if (t - h > 1)
{
for (; step; step >>= 1) if (cnt + step < t)
{
if (slope(stack[cnt + step], stack[cnt + step - 1]) < p[g_Val])
{
cnt += step;
}
}
}
return DP(g_Val, stack[cnt]);
}
int mid = (l + r) >> 1;
LL ret = LLONG_MAX;
if (g_L <= mid) ret = std::min(ret, query_(l, mid));
if (g_R > mid) ret = std::min(ret, query_(mid + 1, r));
return ret;
}
public:
void build()
{
build(1, n);
}
void push(int pos, int val)
{
g_Pos = pos;
g_Val = val;
modify(1, n);
rr.push_back(std::move(curRecord));
}
void restore()
{
std::vector<RestoreRecord> r(std::move(rr.back()));
rr.pop_back();
for (const RestoreRecord& t : r)
{
stack[t.prePos] = t.preVal;
tail[t.stackCode] = t.preTail;
}
}
LL query(int l, int r, int val)
{
g_L = l;
g_R = r;
g_Val = val;
return query_(1, n);
}
} st;
void DFS(int node)
{
wander(G, node)
{
DEF(G);
depth[to] = depth[node] + 1;
Depth[to] = Depth[node] + cost;
dis[0][to] = cost;
for (int i = 1; i <= logn; i++)
{
parent[i][to] = parent[i - 1][parent[i - 1][to]];
dis[i][to] = dis[i - 1][to] + dis[i - 1][parent[i - 1][to]];
}
int cnt = to;
LL remain = l[to];
for (int i = logn; ~i; i--) if (parent[i][cnt])
{
if (remain < dis[i][cnt]) continue;
remain -= dis[i][cnt];
cnt = parent[i][cnt];
}
f[to] = st.query(depth[cnt], depth[node], to);
st.push(depth[to], to);
DFS(to);
st.restore();
}
}
void run()
{
n = readIn();
while (1 << logn < n) logn++;
readIn();
for (int i = 2; i <= n; i++)
{
parent[0][i] = readIn();
cost[i] = readIn();
p[i] = readIn();
q[i] = readIn();
l[i] = readIn();
G.addEdge(parent[0][i], i, cost[i]);
}
st.build();
st.push(1, 1);
depth[1] = 1;
Depth[1] = 0;
DFS(1);
for (int i = 2; i <= n; i++)
printOut(f[i]);
}
int main()
{
run();
return 0;
}
Remarks
一群神仙,都是一句话就把题解写完了,还有不需要任何题解直接贴代码的超级神仙 Orz。还有觉得太简单直接叫看别人博客的神仙 Orz。太强啦!!!我太弱啦!!!