【BZOJ 2152 聪聪可可】【点分治 + 一个套路】

暴力

暴力做法:点分治求出线段,暴力枚举两两线段是否相加 % 3 = 0,会 TLE。

#include <bits/stdc++.h>

using namespace std;
const int N = 2e4    + 5;

struct Edge {
    int next, to, w;
}e[N << 1];

struct seg1 {
    int dis, pos;
}seg1[N << 1];

int sum, rt = 0;
int ans = 0, tot = 0;

bool judge(int x, int y) {
    return ((x + y) % 3 == 0) ? 1 : 0; 
}

int gcd(int a, int b) {
    return (b == 0) ? a : gcd(b, a % b);
}

int cnt = 0;
int head[N];
void add(int x, int y, int z) {
    e[++ cnt].to = y;
    e[cnt].w = z;
    e[cnt].next = head[x];
    head[x] = cnt;
}

int cnt2 = 0;
void add2(int dis, int pos) {
    seg1[++ cnt2].dis = dis;
    seg1[cnt2].pos = pos;
}

int son[N], size[N], vis[N];
void dfs1(int u, int fa) {
    son[u] = 1;
    size[u] = 0;
    for (int i = head[u]; i; i = e[i].next) {
        int v = e[i].to;
        if (v == fa || vis[v]) continue;
        dfs1(v, u);
        son[u] += son[v];
        size[u] = max(size[u], son[v]);
    }

    size[u] = max(size[u], sum - son[u]);
    if (size[u] < size[rt]) rt = u;
}

int dis[N];
void dfs2(int u, int fa, int num) {
    son[u] = 1;
    add2(dis[u], num);
    if (judge(dis[u], 0)) ans ++;
    tot ++;

    for (int i = head[u]; i; i = e[i].next) {
        int v = e[i].to;
        if (v == fa || vis[v]) continue;
        dis[v] = dis[u] + e[i].w;
        dfs2(v, u, num);
        son[u] += son[v];
    }
}

void cal(int u) {
    int temnum = 0; cnt2 = 0;
    for (int i = head[u]; i; i = e[i].next) {
        int v = e[i].to;
        if (vis[v]) continue;
        dis[v] = e[i].w;
        dfs2(v, u, ++ temnum);
    }

    for (int i = 1; i < cnt2; i ++)
        for (int j = i + 1; j <= cnt2; j ++)
            if (seg1[i].pos != seg1[j].pos) {
                tot ++;
                if (judge(seg1[i].dis, seg1[j].dis)) ans ++;
            }
}

void solve(int u) {
    vis[u] = 1;
    cal(u);
    for (int i = head[u]; i; i = e[i].next) {
        int v = e[i].to;
        if (vis[v]) continue;
        size[0] = sum = son[v];
        dfs1(v, rt = 0);
        solve(rt);
    }
}

int main() {
    memset(vis, 0, sizeof(vis));
    int n;
    scanf("%d", &n);
    for (int i = 1; i < n; i ++) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c), add(b, a, c);
    }
    size[0] = sum = n;
    dfs1(1, 0);
    solve(rt);

    ans <<= 1; tot <<= 1; //  a b 和 b a 是两种情况 
    ans += n; tot += n; // 两人选了相同点,距离为 0,有 n 钟可能 

    int tem = gcd(ans, tot);
    ans /= tem; tot /= tem;
    printf("%d/%d\n", ans, tot);
    return 0;
}

优化

我们每次暴力枚举两两线段,是一个 n2 找答案的过程。
我们思考一下,一条余数为 2 的线段加一个余数为 1 的线段。余数肯定为 0
所以我们并不需要记录是哪一条线段,只要知道余数是 0/1/2 的线段有几条就好了。
now[0/1/2] 表示当前子树 % 3 意义下的线段条数,pre[0/1/2] 表示之前子树 % 3 意义下的线段条数。
根据乘法原理不同子树线段直接匹配就可以计算出答案。

一个套路

(这个套路是 小 ly 想出来想出来哒!喵呜

一组数,不重复的两两相乘可以化简为相乘累加的形式。(实质 -> 代码中的表现形式)
例如:

ab+(a+b)c+(a+b+c)d=ab+ac+bc+ad+bd+cd=a(b+c+d)+b(c+d)+cd

这三个等式就是:代码中的表现形式 = 实质 = 通过乘法结合律化简实质后的表现形式

对于本题来说代码中的表现形式中的 a 可以理解为 pre[]b 可以理解为 now[] -> 然后 a + b 成为 pre[]c 成为新的 now[] -> 以此类推 …
实质就是我们暴力两两合并表示余数的线段的过程。
通过乘法结合律化简实质后的表现形式 很好理解嘛就是化简一下辣。

为什么说这是个套路呢
因为它在其他题里也有很好的体现比如:NOIP 2014 D1T2 联合权值

小 ly 的题解w
兄弟关系中算联合权值的和时所有儿子两两排列的乘积就可以按这个套路去做。

#include <bits/stdc++.h>

using namespace std;
const int N = 2e4 + 5;

struct Edge {
    int next, to, w;
}e[N << 1];

struct seg1 {
    int dis, pos;
}seg1[N << 1];

int sum, rt = 0;
int ans = 0, tot = 0;
int now[N], pre[N];

int gcd(int a, int b) {
    return (b == 0) ? a : gcd(b, a % b);
}

int cnt = 0;
int head[N];
void add(int x, int y, int z) {
    e[++ cnt].to = y;
    e[cnt].w = z;
    e[cnt].next = head[x];
    head[x] = cnt;
}

int cnt2 = 0;
void add2(int dis, int pos) {
    seg1[++ cnt2].dis = dis;
    seg1[cnt2].pos = pos;
}

int son[N], size[N], vis[N];
void dfs1(int u, int fa) {
    son[u] = 1;
    size[u] = 0;
    for (int i = head[u]; i; i = e[i].next) {
        int v = e[i].to;
        if (v == fa || vis[v]) continue;
        dfs1(v, u);
        son[u] += son[v];
        size[u] = max(size[u], son[v]);
    }

    size[u] = max(size[u], sum - son[u]);
    if (size[u] < size[rt]) rt = u;
}

int dis[N];
void dfs2(int u, int fa, int num) {
    son[u] = 1;
    dis[u] %= 3;
    now[dis[u]] ++;

    for (int i = head[u]; i; i = e[i].next) {
        int v = e[i].to;
        if (v == fa || vis[v]) continue;
        (dis[v] = dis[u] + e[i].w) %= 3;
        dfs2(v, u, num);
        son[u] += son[v];
    }
}

void cal(int u) {
    pre[0] = 1; // 表示自己这个点 
    ans ++;

    int temnum = 0; cnt2 = 0;
    for (int i = head[u]; i; i = e[i].next) {
        int v = e[i].to;
        if (vis[v]) continue;
        (dis[v] = e[i].w) %= 3;
        dfs2(v, u, ++ temnum);

        for (int i = 0; i < 3; i ++)
            ans += pre[i] * now[(3 - i) % 3] * 2;

        for (int i = 0; i < 3; i ++)
            pre[i] += now[i], now[i] = 0;
    }

    for (int i = 0; i < 3; i ++) pre[i] = 0;
}

void solve(int u) {
    vis[u] = 1;
    cal(u);
    for (int i = head[u]; i; i = e[i].next) {
        int v = e[i].to;
        if (vis[v]) continue;
        size[0] = sum = son[v];
        dfs1(v, rt = 0);
        solve(rt);
    }
}

int main() {
    memset(vis, 0, sizeof(vis));
    int n;
    scanf("%d", &n);
    for (int i = 1; i < n; i ++) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c), add(b, a, c);
    }
    size[0] = sum = n;
    dfs1(1, 0);
    solve(rt);

    tot = n * n;

    int tem = gcd(ans, tot);
    ans /= tem; tot /= tem;
    printf("%d/%d\n", ans, tot);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值