[ZJOI2015]幻想乡战略游戏 - 动态点分治

先考虑无修要怎么操作。

发现在无修的情况下,我们可以用一个换根\(dp\)解决。

那么带修改的情况要怎么办呢?

每次修改重新\(dp\)一遍不就行了(雾。

好的,让我们先来敲一个\(O(N^2)\)的\(dp\)。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll ty() {
  char ch = getchar(); ll x = 0, f = 1;
  while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); }
  while (ch >= '0' && ch <= '9') { x = x * 10 + ch - '0'; ch = getchar(); }
  return x * f;
}

const int _ = 1e5 + 10;
const ll INF = 1e18;
int tot, head[_], to[_ << 1], nxt[_ << 1], edge[_ << 1];

void adde(int x, int y, int z) {
  to[++tot] = y;
  edge[tot] = z;
  nxt[tot] = head[x];
  head[x] = tot;
}

int N, M;
ll val[_], g[_], f[_], sum, ans;

void dfs(int x, int fa, ll d) {
  f[1] += d * val[x];
  g[x] = val[x];
  for (int i = head[x]; i; i = nxt[i]) {
    int y = to[i], z = edge[i];
    if (y == fa) continue;
    dfs(y, x, d + z);
    g[x] += g[y];
  }
}

void dp(int x, int fa) {
  ans = min(ans, f[x]);
  for (int i = head[x]; i; i = nxt[i]) {
    int y = to[i], z = edge[i];
    if (y == fa) continue;
    f[y] = f[x] - g[y] * z + (sum - g[y]) * z;
    dp(y, x);
  }
}

void work() {
  f[1] = 0, ans = INF;
  dfs(1, 0, 0);  //先以1为根,求出答案,然后进行换根dp
  dp(1, 0);
  printf("%lld\n", ans);
}

int main() {
#ifndef ONLINE_JUDGE
  freopen("fantasy.in", "r", stdin);
  freopen("fantasy.out", "w", stdout);
#endif
  N = ty(), M = ty();
  for (int i = 1; i < N; ++i) {
    int x = ty(), y = ty(), z = ty();
    adde(x, y, z);
    adde(y, x, z);
  }
  while (M--) {
    int x = ty(), y = ty();
    val[x] += y, sum += y;
    work();  // 每修改一次,就重新dp一次2333
  }
  return 0;
}

然后就是这样的盛况:

“没事,卡卡常就过了。”@mzx

因为我太菜了,所以并没有把\(O(N^2)\)卡常卡成\(O(N \log N)\)的能力。

先考虑一下这个\(dp\)的本质是什么,考虑从一个点\(x\)移动到点\(y\),那么花费值的变化亮就是
\[
dis_{x,y} \times (sum - 2 \times g_y)
\]
所以我们发现,当\(sum-2\times g_y\)为负数的时候,答案便会减小,且可以证明每次这样的点唯一,所以实际上,我们可以从\(root\)开始出发,每次往\(sum - 2 \times g_y\)为负数的那个方向走,直到走不动时,就得到了最优解。

下面是动态点分治的内容了

发现主要是每次修改后,都需要从\(root\)节点重新开始寻找最优解,最坏情况下还是会达到\(O(N^2)\)的复杂度,所以考虑如何优化这个过程。此时,就应该扯上点分树了。

假设我们能利用点分树求得答案,因为在点分树上只需要访问\(\log\)个点,所以一次修改的复杂度就降到了\(\log\)级别。

此时,正确的做法似乎逐渐浮出了水面:

假设当前所在的节点为\(x\),那么我们枚举\(x\)在原树上的边,假设这条边的另一端为\(y\),然后用\(O(\log N)\)的复杂度暴力计算出如果移动到\(y\),花费是多少,如果花费减小,那么跳到点分树中对应的块上,然后再在对应的块里面求解,若不存在这样的边,那么肯定就是最优解。

现在思考一下这个做法需要维护哪些东西:

  1. 每个块的点权和\(sum_x\)(点分树上\(O(\log N)\)修改)
  2. \(dist_x\):\(x\)在点分树上对应的联通块所有的点跳到\(x\)的花费
  3. \(sub_x\):\(x\)在点分树上对应的联通块所有的点跳到\(x\)在点分树上的父亲\(fa_x\)的花费

然后查询和修改不断暴跳父亲就好了。

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

inline int ty() {
    char ch = getchar(); int x = 0, f = 1;
    while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); }
    while (ch >= '0' && ch <= '9') { x = x * 10 + ch - '0'; ch = getchar(); }
    return x * f;
}

typedef long long ll;
const int _ = 1e5 + 10;
const int INF = 0x3f3f3f3f;
int N, M;
int tot, head[_], to[_ << 1], nxt[_ << 1], edge[_ << 1];

void adde(int x, int y, int z) {
    to[++tot] = y;
    edge[tot] = z;
    nxt[tot] = head[x];
    head[x] = tot;
}

namespace lca {
int fa[_], son[_], dep[_], dis[_], siz[_], top[_];
void dfs1(int x, int f) {
    siz[x] = 1;
    int maxx = 0;
    for (int i = head[x]; i; i = nxt[i]) {
        int y = to[i], z = edge[i];
        if (y == f) continue;
        fa[y] = x, dep[y] = dep[x] + 1, dis[y] = dis[x] + z;
        dfs1(y, x);
        siz[x] += siz[y];
        if (siz[y] > maxx) maxx = siz[y], son[x] = y;
    }
}
void dfs2(int x, int topf) {
    top[x] = topf;
    if (!son[x]) return;
    dfs2(son[x], topf);
    for (int i = head[x]; i; i = nxt[i]) {
        int y = to[i];
        if (y == fa[x] || y == son[x]) continue;
        dfs2(y, y);
    }
}
void init() {
    dep[1] = 1, dfs1(1, 0);
    dfs2(1, 1);
}
int query(int x, int y) {
    while (top[x] != top[y]) {
        if (dep[top[x]] < dep[top[y]]) swap(x, y);
        x = fa[top[x]];
    }
    return dep[x] < dep[y] ? x : y;
}
int dist(int x, int y) { return dis[x] + dis[y] - 2 * dis[query(x, y)]; }
} // namespace lca

int mxsiz, totsiz, root, siz[_], vis[_], fa[_];

void getroot(int x, int f) {
    siz[x] = 1;
    int maxx = 0;
    for (int i = head[x]; i; i = nxt[i]) {
        int y = to[i];
        if (vis[y] || y == f) continue;
        getroot(y, x);
        siz[x] += siz[y];
        maxx = max(maxx, siz[y]);
    }
    maxx = max(maxx, totsiz - siz[x]);
    if (maxx < mxsiz) mxsiz = maxx, root = x;
}

typedef pair<int, int> PII;
vector<PII> E[_];

void divide(int x) {
    vis[x] = true;
    int nowsiz = totsiz;
    for (int i = head[x]; i; i = nxt[i]) {
        int y = to[i];
        if (vis[y]) continue;
        mxsiz = INF, root = 0;
        totsiz = siz[y] > siz[x] ? nowsiz - siz[x] : siz[y];
        getroot(y, 0);
        E[x].push_back(make_pair(y, root));
        fa[root] = x;
        divide(root);
    }
}

ll sum[_];  // 点分树中以x为根的子树的权值和
ll dist[_]; // 点分树中以x为根的子树全部到x的花费
ll sub[_];  // 点分树中以x为根的子树全部到x在点分树上的父亲的花费

void modify(int x, int y) {
    sum[x] += y;
    for (int i = x; fa[i]; i = fa[i]) {
        int len = lca::dist(x, fa[i]);
        sum[fa[i]] += y;
        dist[fa[i]] += 1ll * y * len;
        sub[i] += 1ll * y * len;
    }
}

// 用一个log的代价直接计算以x为供应站时的话费
ll calc(int x) {
    ll ret = dist[x];
    for (int i = x; fa[i]; i = fa[i]) {
        int len = lca::dist(x, fa[i]);
        ret += 1ll * len * (sum[fa[i]] - sum[i]);
        ret += dist[fa[i]] - sub[i];
    }
    return ret;
}

ll query(int x) {
    // printf("%d\n", x);
    ll cur = calc(x);
    for (auto p : E[x]) {
        int y = p.first, rt = p.second;
        // printf("%d %d\n", y, rt);
        if (calc(y) < cur) return query(rt);
    }
    return cur;
}

int main() {
#ifndef ONLINE_JUDGE
    freopen("fantasy.in", "r", stdin);
    freopen("fantasy.out", "w", stdout);
#endif
    N = ty(), M = ty();
    for (int i = 1; i < N; ++i) {
        int x = ty(), y = ty(), z = ty();
        adde(x, y, z);
        adde(y, x, z);
    }
    lca::init();

    mxsiz = INF, totsiz = N, root = 0;
    getroot(1, 0);
    int RT = root;
    divide(root);

    while (M--) {
        // printf("!%d\n", M);
        int x = ty(), y = ty();
        modify(x, y);
        printf("%lld\n", query(RT));
    }
    return 0;
}

原文地址:https://www.cnblogs.com/newbielyx/p/12129692.html

时间: 2024-08-04 03:45:40

[ZJOI2015]幻想乡战略游戏 - 动态点分治的相关文章

【bzoj3924】[Zjoi2015]幻想乡战略游戏 动态树分治

题目描述 傲娇少女幽香正在玩一个非常有趣的战略类游戏,本来这个游戏的地图其实还不算太大,幽香还能管得过来,但是不知道为什么现在的网游厂商把游戏的地图越做越大,以至于幽香一眼根本看不过来,更别说和别人打仗了. 在打仗之前,幽香现在面临一个非常基本的管理问题需要解决. 整个地图是一个树结构,一共有n块空地,这些空地被n-1条带权边连接起来,使得每两个点之间有一条唯一的路径将它们连接起来.在游戏中,幽香可能在空地上增加或者减少一些军队.同时,幽香可以在一个空地上放置一个补给站. 如果补给站在点u上,并

BZOJ 3924 Zjoi2015 幻想乡战略游戏 动态树分治

题目大意:给定一棵树,每个点有一个点权,多次改变某个点的点权,多次查询带权重心到所有点的带权距离之和 此生无悔入东方,来世愿生幻想乡 首先我们考虑如何计算一个点到所有点的带权距离之和且支持修改 用动态树分治就好了嘛... 每个点记录子树中带权距离之和,以及权值之和,再在每个子树中记录一个需要减掉的版本 然后一直向上扫到根就能统计了 ↑这段话面对会写动态树分治的人,不会的先去切捉迷藏吧 然后就好搞了... 对于分治结构的每一个点,我们枚举它的出边 如果某条出边连向的点的距离之和小于当前点,那么答案

BZOJ 3924: [Zjoi2015]幻想乡战略游戏(动态点分治)

这种动态点分治嘛,GDKOI时听打到了,也有同学讲到了,所以印象比较深刻也就想出来了,然后就在实现方面卡了好久= = 不得不说CLJ说得真的太简单了,实现方面根本没提. 首先我们可以先用树分治构建出这棵树的分治树,也就是把这棵树的重心作为根节点然后子树为他的子树的重心这样递归下去,然后每个节点存的是其子树的信息. 对于每个节点我们保存这个子树的dv的总和已经把该节点作为点的答案值 这样对于修改能在log n的时间内解决 寻找答案的时候,我们可以发现,如果现在节点的子树dv和*2大于总节点,那么向

luogu P3345 [ZJOI2015]幻想乡战略游戏 |动态点分治

题目描述 傲娇少女幽香正在玩一个非常有趣的战略类游戏,本来这个游戏的地图其实还不算太大,幽香还能管得过来,但是不知道为什么现在的网游厂商把游戏的地图越做越大,以至于幽香一眼根本看不过来,更别说和别人打仗了. 在打仗之前,幽香现在面临一个非常基本的管理问题需要解决. 整个地图是一个树结构,一共有n块空地,这些空地被n-1条带权边连接起来,使得每两个点之间有一条唯一的路径将它们连接起来. 在游戏中,幽香可能在空地上增加或者减少一些军队.同时,幽香可以在一个空地上放置一个补给站. 如果补给站在点u上,

loj 2135 「ZJOI2015」幻想乡战略游戏 - 动态点分治

题目传送门 传送门 题目大意 给定一棵树,初始点权都为0,要求支持: 修改点权 询问带权重心 询问带权重心就在点分树上跑一下就行了.(枚举跳哪个子树更优) 剩下都是基础点分治. 学了一下11-dimensional的2.2k动态点分治,然后写抄出来只有1.9k??? Code /** * loj * Problem#2135 * Accepted * Time: 4492ms * Memory: 28404k */ #include <bits/stdc++.h> using namespac

[ZJOI2015]幻想乡战略游戏 解题报告 (动态点分治)

[ZJOI2015]幻想乡战略游戏 题意 有一棵大小为 \(n\) 的带权树, 每个点有一个权值, 权值可以修改 \(q\) 次, 找出一个补给点 \(x\) , 使得 \(\sum_{u \in V} val[u] \times dis(x,u)\) 最小, 并求出这个最小值. 一句话 : 求带权重心 (zsy说的) 附加条件 : 树中所有点的度数不超过 \(20\). 思路 一道你以为复杂度过不了, 但其实是过得了的题. 首先, 我们假定先选了一个点 \(u\) 作为补给点, 我们可以算出它

bzoj3924 [Zjoi2015]幻想乡战略游戏 点分树,动态点分

[BZOJ3924][Zjoi2015]幻想乡战略游戏 Description 傲娇少女幽香正在玩一个非常有趣的战略类游戏,本来这个游戏的地图其实还不算太大,幽香还能管得过来,但是不知道为什么现在的网游厂商把游戏的地图越做越大,以至于幽香一眼根本看不过来,更别说和别人打仗了. 在打仗之前,幽香现在面临一个非常基本的管理问题需要解决. 整个地图是一个树结构,一共有n块空地,这些空地被n-1条带权边连接起来,使得每两个点之间有一条唯一的路径将它们连接起来.在游戏中,幽香可能在空地上增加或者减少一些军

[Luogu3345][ZJOI2015]幻想乡战略游戏

Luogu 题意: 动态维护带权重心. sol 这是一道写起来很舒服的动态点分治.(不像某些毒瘤题) 我们考虑,如果你选择的补给点不是当前的带权重心,那么带权重心就在补给点的一个子树中(你把补给点当做根的话).那么,你把补给点向带权重心所在的子树中移动的时候,答案一定会减小.换言之,如果补给点无论向哪个方向移动答案都不会减小,那么这个点就是带权重心. 所以我们每次考虑移动补给点然后计算即可. 但是怎么移动呢? 最优复杂度的移动策略是:先假设点分树的根就是补给,然后你一次检查与它在原树中相连的所有

luogu P3345 [ZJOI2015]幻想乡战略游戏(点分树)

题意自己看... 思路 没想到今(昨)天刷着刷着点分治的水题,就刷出来了一个点分树... 然后就疯狂地找题解,代码,最后终于把它给弄懂了. 点分树--动态点分治,对于此题来说,我们发现设u为当前的补给站位置,v是它的一个儿子.同时设dis(i,j)为树上i点到j点的距离.sumi为以i为跟的子树中d(也就是军队数)的总量 我们把补给站从u转移到v,答案的变化为dis(u,v)*(sumu-sumv)-dis(u,v)*sumv=dis(u,v)*(sumu-2*sumv) 所以当2*sumv>s