Luogu 2305 [NOI 2014] 购票

传送门
思路

唉,我太弱了,什么都不会,斜率优化也不会,唉,我太弱啦!

显然可以 O ( n 2 ) DP,这样就得到了 30 分。考虑斜率优化。设我们正在计算 f i ,可能的决策点 j 比可能的决策点 k 更深。若 j k 更优,我们有:

f j + ( d i d j ) p i + q i < f k + ( d i d k ) p i + q i

化简得:

f j f k d j d k < p i

由于 p i 并不单调,因此我们需要在凸壳上二分。但显然我的知识水平是不够的,因为题目还有一个 l 的限制。下面我们来逐一解决……

别人家的题解

唉,我太弱了,别人的题解一句话就把这道题切了,我写两页都切不了,别人写的我又看不懂,根本没有学过那些黑科技,唉,我太弱啦!

别人家的题解写道:方法 1:线段树维护可持久化单调队列;方法 2:点分治

QAQ 全都不会啊。

不如我们先看一下这道题的弱化版。

弱化的传送门(Luogu 3994 高速公路)

哪里弱化了:没有距离限制 l ,且 p i 是递增的。

做了弱化版的题有助于做这道题吗?当然不可能 QAQ

我们仍然有斜率优化的不等式:

f j f k d j d k < p i

由于 p i 是递增的,因此,如果这棵树是一条链,那么这就是一个裸的单调队列斜率优化 DP。现在在树上,可以怎么做呢?

我们考虑可持久化。当然,这个可持久化是概念上的,我们还没有规定要用什么具体的数据结构。我们考虑直接用数组维护这个单调队列。观察发现,这个单调队列实际上可以看成一个单调栈(因为我们没有向队头插入元素,所以队头那一部分的数据是不受影响的),所以我们只需要考虑如何维护队尾就好了。当我们遍历到一个结点上时,我们会弹出一些元素,然后入队一个元素。弹出元素的操作可以通过修改队尾下标来实现,也就是说队列中的数据实际上还在。但是入队一个元素会导致一个数据被覆盖,我们记录一下这个被覆盖的数据是什么就好了。当我们需要得到父结点对应的版本时,我们恢复被覆盖的那个数据,将队头和队尾的下标还原即可。

但是这样做每个元素的出队次数不能使用均摊分析。考虑二分出队位置,这样我们就能在 O ( n log n ) 的时间复杂度内解决这个问题了。

参考代码
#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 分做法来:

示意图 1

我们考虑这么一个做法:还是用可持久化单调栈(弱化版题目用的那个),但是为当前链上的每一个元素都开一个单调栈。假设最远只能到达 j ,那么我们找到 j 对应的那个单调栈,在上面二分,这样正确性就有了保证。

我们来分析一下时间复杂度。修改单个单调栈的时间复杂度是 O ( log n ) 的(不要忘了不能使用均摊分析),总共有 O ( n ) 个单调栈需要修改,因此整个算法的时间复杂度是 O ( n 2 log n )

考虑优化,这里我就直接给出如何优化的,我们用线段树结构进行优化。具体地说,我们维护一棵线段树,线段树上的每个结点都对应一个单调栈,那么空间复杂度显然就是 O ( n log n ) 的。设线段树上的结点的左端点为 l ,每个单调栈保存的内容为深度大于等于 l 的结点对应的单调栈。每次修改时,我们修改所有覆盖了待修改深度的结点,总共有 O ( log n ) 个,每个要花费 O ( log n ) 的时间去修改。查询时,我们查询可以到达的结点深度对应的区间,总共要查 O ( log n ) 次,每个要花费 O ( log n ) 的时间。深搜结束时,我们要花费 O ( log n ) 的时间复杂度还原。所以总时间复杂度为 O ( n log 2 n )

为什么可以像这样分成多个单调栈来查询呢?原因其实很简单。我们使用单调栈的目的是排除无用决策,显然的是,最终答案对应的决策点一定不会是无用决策,所以它在任何一个单调栈中都不会被排除。因此,最优决策点一定在 O ( log n ) 个单调栈中的一个里。

参考代码
#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。太强啦!!!我太弱啦!!!

猜你喜欢

转载自blog.csdn.net/lycheng1215/article/details/80351839
今日推荐