「JOISC 2017 Day 3」自然公园 题解

因为感觉网上题解很少,所以想来写一点感性+理性混合的题解qwq

而且犯了很多弱智错误)

考虑我们可以以以下形式确定每一条边:

我们维护一个包含0号节点的连通块 每次挑选一个点 x x x,满足 x x x 有和连通块直接相连的边

我们的任务就是:如何确定这样一条边?

直接扫肯定是不可接受的,所以我们采用二分的形式:

确定一个序(dfs/bfs)均可,然后在这个序上进行二分,二分出哪个前缀是满足联通的最小前缀(就是只包含这个前缀里的节点时,可以满足仍然有直接连边)

这样,前缀里的最后一个点,就是和这个点有直接连边的点

然后删去这个直接相连的点,会分出若干连通块,再检验 x x x 和这些连通块是否有直接连边

考虑为什么检验次数是对的,首先显然会有 M l o g n Mlogn Mlogn 次检验(不过这个log应该比较小)

然后考虑,删了一个点之后会分出若干连通块,就算立马返回,也要一次check

那么对于菊花来说,复杂度是否正确呢?

因为题目限制最多7条边,所以最多分出六个连通块,也就是总复杂度应该是 7 M l o g 7Mlog 7Mlog

鉴于很难每个点都跑到上届,所以可过)

那么另一个问题 我们如何找到一个 x x x 呢?

事实上 我们每次可以随机联块外一个点 x x x,随机的必要性在于防止被精心构造的数据多卡出一个 N l o g Nlog Nlog的check

然后对于这个 x x x 我们可以找到它和连通块之间的一点 y y y

但注意 我们应该标记连通块外的所有点,当以它去找路径中点的时候,应该把它删掉,理由有2:

1.他已经不可能成为其他的点的,路径上的必须点了(再搜完它前面的点之后 它会直接成为连通块,而它不可能成为它前面的点的必须点)

2.避免连通块之间胡搜,可能会出现(测试出的)连通块之间互搜的情况

#include "park.h"
#include <bits/stdc++.h>
using namespace std;
int const N = 1410;
struct stu1 {
    int y, nex;
} e[N * 5];
int in[N], out[N], t[N], lin[N], tot, cnt, tim, vis[N], q[N], num, pl[N], n;
bool check(int x) {
    in[x] = 1;
    int op = Ask(0, x, in);
    in[x] = 0;
    return op;
}
void bfs(int x) {
    int l = 1, r = 1;
    q[1] = x;
    ++tim;
    int dq, y;
    tot = 0;
    vis[x] = tim;

    while (l <= r) {
        dq = q[l++];
        t[++tot] = dq;

        for (int i = lin[dq]; i; i = e[i].nex) {
            y = e[i].y;

            if (in[y] && vis[y] < tim) {
                q[++r] = y;
                vis[y] = tim;
            }
        }
    }
}
bool ask(int x, int y, int lim) {
    memset(pl, 0, sizeof(pl));

    for (int i = 1; i <= lim; i++)
        pl[t[i]] = 1;

    pl[y] = 1;
    return Ask(min(x, y), max(x, y), pl);
}
void link(int x, int y) {
    e[++cnt] = (stu1) {
        y, lin[x]
    };
    lin[x] = cnt;
}
void extend(int ro, int x) {
    bfs(ro);//删了一个点之后 会变成若干连通块 (等等 对于菊花来说 复杂度为什么是对的... 因为只有七条边所以复杂度是对的)

    if (!ask(ro, x, tot))
        return;

    int mid, l = 1, r = tot; //找到序最小的一个直接相连的位置

    while (l < r) {
        mid = (l + r) >> 1;

        if (ask(ro, x, mid))
            r = mid;
        else
            l = mid + 1;
    }

    int z = t[l], dfn = tim;
    in[z] = 0;

    for (int i = lin[z]; i; i = e[i].nex) {
        int y1 = e[i].y; //注意 我这里写的和参考程序有出入 参考程序开多个p的目的就在于这里

        if (in[y1] && vis[y1] <= dfn)
            extend(y1, x);
    }

    in[z] = 1;
    link(z, x);
    link(x, z);
    Answer(min(x, z), max(x, z));
}
void work(int
          x) { //把它调出out集合的原因是: 虽然不会出现xy直接互搜的情况 但是可能会出现循环节互搜的情况..
    //而且调出去一定不错 目的是找到和连通块直接连接的点 可以确定这个点不和连通块直接连接
    out[x] = 0;
    int mid, l, r, d = 0;
    tot = 0;

    while (!check(x)) {
        d = 0; //while可能会跑多次 要清d

        for (int i = 0; i < n; i++)
            if (in[i])
                t[++d] = i;

        l = d + 1;

        for (int i = 0; i < n; i++)
            if (out[i])
                t[++d] = i;

        r = d;

        while (l < r) {
            mid = (l + r) >> 1;

            if (ask(0, x, mid))
                r = mid;
            else
                l = mid + 1;
        }

        work(t[l]);
    }

    extend(0, x);
    num++;
    in[x] = 1; //找出所有x向连通块里连的边
}
void Detect(int T, int
            N) { //我不是很懂为什么要rand一个点出来 所以我不rand  盲猜一下 应该是怕被精心构造的数据卡之类的?
    in[0] = 1;
    num = 1;
    n = N; //还是需要rand的 不然最劣可以跑到8Mlog左右(大概)

    for (int i = 1; i < N; i++)
        out[i] = 1;

    int x;

    while (num < N) {
        x = rand() % N;

        while (in[x]) {
            x = rand() % N; //最坏n^2次 总之应该是能随完的)
        }

        work(x); //处理i号点和连通块里所有点的连边 然后把它加连通块里
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值