JOISC 2020 题解

作者: xht37 分类: 题解 发布时间: 2020-03-25 16:17

Visits: 606

JOISC 2020

Day1

ビルの飾りつけ 4 (Building 4)

先考虑 $n \le 2 \times 10^3$ 怎么做。

设 $f_{i,j,0/1}$ 表示考虑前 $i$ 个数,在 $a$ 中选了 $j$ 个数,第 $i$ 个数选的是 $a/b$ 是否可行。

那么 $\mathcal O(n^2)$ dp 即可,如果有方案,从后往前构造。

注意到对于 $f_{i,j,0/1}$,如果 $i,0/1$ 固定,$j$ 可行的方案为一个区间。

因此只用 $\mathcal O(n)$ dp 出对于每个 $i,0/1$,$f_{i,j,0/1}$ 中 $j$ 的可行区间。

const int N = 5e5 + 7, inf = 1e9;
int n, a[N<<1], b[N<<1];
pi f[N<<1][2];

inline void upd(pi &a, pi b, bool c) {
    a.fi = min(a.fi, b.fi + c), a.se = max(a.se, b.se + c);
}

inline bool pd(pi a, int x) {
    return a.fi <= x && a.se >= x;
}

int main() {
    rd(n), rda(a, n << 1), rda(b, n << 1);
    f[1][0] = mp(1, 1), f[1][1] = mp(0, 0);
    for (int i = 2; i <= (n << 1); i++)
        for (int j = 0; j < 2; j++)
            f[i][j] = mp(inf, -inf);
    for (int i = 1; i < (n << 1); i++)
        for (int j = 0; j < 2; j++)
            if (f[i][j] != mp(inf, -inf)) {
                if (j == 0 && a[i+1] >= a[i])
                    upd(f[i+1][0], f[i][j], 1);
                if (j == 1 && a[i+1] >= b[i])
                    upd(f[i+1][0], f[i][j], 1);
                if (j == 0 && b[i+1] >= a[i])
                    upd(f[i+1][1], f[i][j], 0);
                if (j == 1 && b[i+1] >= b[i])
                    upd(f[i+1][1], f[i][j], 0);
            }
    if (!pd(f[n<<1][0], n) && !pd(f[n<<1][1], n)) return print(-1), 0;
    int p = n, q = 0;
    string s;
    a[n<<1|1] = b[n<<1|1] = inf;
    for (int i = n << 1; i; i--)
        if (pd(f[i][0], p) && a[i] <= (q ? b[i+1] : a[i+1]))
            s += 'A', --p, q = 0;
        else s += 'B', q = 1;
    reverse(s.begin(), s.end());
    prints(s);
    return 0;
}

美味しい美味しいハンバーグ (Hamburg Steak)

ouuan 的做法,不知道为啥能过。(upd:现在似乎已经被卡掉了)

首先随机 $k$ 个矩形,然后依次将剩下的矩形与 $k$ 个矩形求交,哪个剩下的面积与原来的面积之最大就选择哪个。

多次随机直到找到解,可以通过本题全部数据。

const int N = 2e5 + 7;
const ld eps = 1e-10L;
int n, k;
struct P {
    int l, d, r, u;
    inline P() {}
    inline P(int l, int d, int r, int u) : l(l), d(d), r(r), u(u) {}
    inline void in() { rd(l), rd(d), rd(r), rd(u); }
    inline ll s() {
        return (l > r || d > u) ? 0ll : 1ll * (r - l + 1) * (u - d + 1);
    }
} p[N];
vector<P> a;

inline ll calc(P a, P b) {
    a.l = max(a.l, b.l);
    a.d = max(a.d, b.d);
    a.r = min(a.r, b.r);
    a.u = min(a.u, b.u);
    return a.s();
}

inline void upd(P &a, P b) {
    a.l = max(a.l, b.l);
    a.d = max(a.d, b.d);
    a.r = min(a.r, b.r);
    a.u = min(a.u, b.u);
}

inline bool work() {
    a.clear();
    map<int, bool> v;
    for (int i = 0; i < k; i++) {
        int x = rdm(1, n);
        while (v[x]) x = rdm(1, n);
        v[x] = 1, a.pb(p[x]);
    }
    for (int i = 1; i <= n; i++)
        if (v.find(i) == v.end()) {
            ld x = 0.0L;
            int o = -1;
            for (int j = 0; j < k; j++) {
                ld y = 1.0L * calc(a[j], p[i]) / a[j].s();
                if (y > x + eps) x = y, o = j;
            }
            if (!~o) return 0;
            upd(a[o], p[i]);
        }
    return 1;
}

int main() {
    srand(time(0));
    rd(n), rd(k);
    for (int i = 1; i <= n; i++) p[i].in();
    while (!work());
    for (int i = 0; i < k; i++) print(a[i].l, ' '), print(a[i].d);
    return 0;
}

掃除 (Sweeping)

写一个可能过不了的做法。。。

先考虑没有操作 $4$,即没有加入新点的情况。

首先有一个显然的结论:对于至少被移动过一次的点 $i,j$,一定不会出现 $x_i < x_j$ 且 $y_i < y_j$ 的情况。

因此,如果我们对至少被移动过一次的点,以横坐标从小到大为第一关键字,纵坐标从大到小为第二关键字排序,则每次移动的一定是一个区间,且移动前后排序关系不变。

考虑移动的本质为,将某一个区间里的点的横坐标或纵坐标对一个定值取 $\max$。

于是我们可以用一个支持区间修改,单点查询的数据结构维护,比如线段树。

那对于没有被移动过的点怎么办呢?

我们可以找到每个点第一次移动的时间,然后在那个时间插入到线段树中……

插入到线段树?这件事显然做不到,因此我们还要将线段树换成平衡树。

如何找到每个点第一次移动的时间呢?

由于移动涉及到的点都在一个矩形内,因此可以用可持久化线段树维护对于每个纵坐标,任意时刻受到影响的最大横坐标。然后对于每个点,在主席树上二分即可。

查询时,直接找到一个点在平衡树中的点,然后从这个点找到根再回溯,过程中下传区间修改时的标记即可。

设 $m,q$ 同阶,时间复杂度 $\mathcal O(m \log^2 m)$。

如果有加入新点的操作,考虑二进制分组维护这个过程。

即,假设当前有 $k$ 个点,按照 $k$ 的二进制位下 $1$ 的位置进行分组。比如如果 $k = 21$,则分成 $16+4+1$ 三组。显然,组的个数是 $\mathcal O(\log k)$ 的。

对于每组内部维护没有加入新点时的情况。当加入一个新点时,分成的组会从后往前合并,合并的同时暴力重构这一组即可。

由于每个节点至多被重构 $\mathcal O(\log m)$ 次,总时间复杂度为 $\mathcal O(m \log^3 m)$。

代码不写了。。。

Day2

カメレオンの恋 (Chameleon’s Love)

对于两个点 $x,y$,如果我们按照如下方式建图:

  • 若 $x,y$ 颜色相同,则 $x,y$ 之间连一条无向边,记为 $x \leftrightarrow y$。
  • 若 $x$ 喜欢 $y$,则连一条 $x \to y$ 的有向边。

先不考虑询问的限制次数。

如果我们询问所有的点对 $(x,y)$,返回的结果要么为 $1$,要么为 $2$。

如果为 $1$,则只有以下三种情况:

  1. $x,y$ 颜色相同。
  2. $x$ 喜欢 $y$,但 $y$ 不喜欢 $x$。
  3. $y$ 喜欢 $x$,但 $x$ 不喜欢 $y$。

容易发现,当答案为 $1$ 时,$x,y$ 之间一定有边;答案为 $2$ 时,$x,y$ 之间一定无边,除了 $x \to y$ 且 $y \to x$ 的情况。

进一步可以发现,每个点的度数都是 $3$,但是询问出来的度数可能为 $1$。

若询问出来 $x$ 的度数为 $1$,则说明存在一个度数同样为 $1$ 的点 $y$,符合 $x \to y$ 且 $y \to x$ 的情况,而询问出来的那条边,则是一条无向边 $x \leftrightarrow z$,这意味着 $x,z$ 的颜色相同。

若询问出来 $x$ 的度数为 $3$,设这 $3$ 个点为 $y,z,w$。考虑到如果我们询问 $(x,y,z),(x,y,w),(x,z,w)$,其中有且仅有一组的答案为 $1$,其它两组的答案都为 $2$。

不妨设为 $1$ 的询问为 $(x,y,z)$,则只可能是 $x \leftrightarrow y$ 且 $z \to x$,或者 $x \leftrightarrow z$ 且 $y \to x$。

尽管我们无法得知是哪种,但是我们可以以此确定 $x \to w$ 这条边。

通过这种方法,我们可以确定所有的有向边,那么剩下的边就是无向边,即颜色相同的点。

这个做法的询问次数为 $\mathcal O(n^2)$ 的,瓶颈在询问两两点对上。

考虑优化这个过程。

我们可以通过 $\mathcal O(n)$ 次询问求出一个极大独立集。

方法为,从 $1 \sim 2n$ 依次尝试加入当前的极大独立集 $S$,如果询问 $(S,x)$ 的答案为 $|S| + 1$,则说明可以加入,否则说明有边。

找到极大独立集之后,对于独立集之外的点,在独立集中二分找到所有边。

此时还剩下独立集之外的点中的边没有找到,那么重复上述过程,直到所有剩下的点都是一个独立集。

由于每个点的度数最多为 $3$,因此每次独立集的大小 $\ge$ 总点数的 $\frac 14$。这意味着每次我们可以将点的规模缩小到原来的 $\frac 34$,那么一共只需要最多重复 $\mathcal O(\log n)$ 次就能找到所有边,因此求最大独立集部分的询问次数为 $\mathcal O(n \log n)$。

又由于每找到一条边需要一次二分,而边数是 $\mathcal O(n)$ 的,所以二分部分也只有 $\mathcal O(n \log n)$ 次询问。

这样我们就可以用 $\mathcal O(n \log n)$ 次询问找到所有边,为了减小常数防止被卡,可以先对点进行 random_shuffle

inline int get(int x, vi p) {
    int l = 0, r = p.size() - 1;
    while (l < r) {
        int mid = (l + r) >> 1;
        vi q(1, x);
        for (int i = l; i <= mid; i++) q.pb(p[i]);
        if (Query(q) != (int)q.size()) r = mid;
        else l = mid + 1;
    }
    return p[l];
}

inline int ask(int x, int y, int z) {
    vi p(3);
    p[0] = x, p[1] = y, p[2] = z;
    return Query(p);
}

void Solve(int n) {
    srand(time(0));
    vi A, B;
    for (int i = 1; i <= (n << 1); i++) B.pb(i);
    random_shuffle(B.begin(), B.end());
    vector<vi> e(n << 1 | 1);
    while (B.size()) {
        vi S = B, v(n << 1 | 1);
        A.clear(), B.clear();
        for (int x : S) {
            A.pb(x);
            if (Query(A) != (int)A.size()) A.pop_back(), B.pb(x);
            else v[x] = 1;
        }
        for (int i : B) {
            vi p = A;
            p.pb(i);
            do {
                p.pop_back();
                int k = get(i, p);
                e[k].pb(i), e[i].pb(k);
                for (int &x : p)
                    if (x == k) swap(x, p.back());
                p.pop_back();
                p.pb(i);
            } while (Query(p) != (int)p.size());
        }
    }
    vi v(n << 1 | 1);
    vector<map<int, bool>> f(n << 1 | 1);
    for (int x = 1; x <= (n << 1); x++)
        if (e[x].size() == 3u) {
            int y = e[x][0], z = e[x][1], w = e[x][2];
            if (ask(x, y, w) == 1) swap(z, w);
            else if (ask(x, z, w) == 1) swap(y, w);
            f[x][w] = f[w][x] = 1;
        }
    for (int x = 1; x <= (n << 1); x++)
        if (!v[x])
            for (int y : e[x])
                if (!f[x][y]) v[x] = v[y] = 1, Answer(x, y);
}

ジョイッターで友だちをつくろう (Making Friends on Joitter is Fun)

并查集维护每个极大团即可。

如果某一次加边后,存在两个团之间可以互相到达,则需要合并这两个团。

set 维护每个团内的节点,点团之间的边,和团团之间的边,合并时启发式合并。

时间复杂度 $\mathcal O(n \log^2 n)$。

const int N = 1e5 + 7;
int n, m, f[N];
set<int> s[N], e[N], g[N], fe[N], fg[N];
deque<int> X, Y;
ll ans;

int get(int x) {
    return x == f[x] ? x : (f[x] = get(f[x]));
}

inline void work(int x, int y) {
    int fx = get(x), fy = get(y);
    if (fx == fy) return;
    if (fe[fy].find(fx) == fe[fy].end()) {
        if (g[fy].find(x) == g[fy].end())
            ans += s[fy].size(),
            e[x].insert(fy), g[fy].insert(x),
            fe[fx].insert(fy), fg[fy].insert(fx);
        return;
    }
    ans -= 1ll * s[fx].size() * (s[fx].size() - 1);
    ans -= 1ll * s[fy].size() * (s[fy].size() - 1);
    fe[fy].erase(fx), fg[fx].erase(fy);
    if (s[fx].size() < s[fy].size()) swap(fx, fy);
    for (int i : s[fy]) {
        for (int j : e[i])
            ans -= s[j].size(),
            g[j].erase(i),
            X.pb(i), Y.pb(j);
        e[i].clear();
    }
    for (int i : g[fy])
        ans -= s[fy].size(),
        e[i].erase(fy),
        X.pb(i), Y.pb(fx);
    g[fy].clear();
    for (int i : fe[fy]) fg[i].erase(fy);
    for (int i : fg[fy]) fe[i].erase(fy);
    for (int i : s[fy]) s[fx].insert(i);
    f[fy] = fx;
    ans += 1ll * s[fx].size() * (s[fx].size() - 1);
    ans += 1ll * g[fx].size() * s[fy].size();
    s[fy].clear();
}

int main() {
    rd(n), rd(m);
    for (int i = 1; i <= n; i++) f[i] = i, s[i].insert(i);
    for (int i = 1, x, y; i <= m; i++) {
        rd(x), rd(y), X.pb(x), Y.pb(y);
        while (X.size()) work(X[0], Y[0]), X.pop_front(), Y.pop_front();
        print(ans);
    }
    return 0;
}

最古の遺跡 3 (Ruins 3)

题目太神仙了,先咕咕咕着。

Day3

星座 3 (Constellation 3)

考虑从上往下选,则如果选择了一个星星,那下面就由一些星星不能选了。

建出笛卡尔树之后可以发现,笛卡尔树上面的每个节点都对应着一个极大矩形,而一棵星星所在的极大矩形,在笛卡尔树上正好是一条,从某个节点到其子树内某个叶子的路径。

因此对于一个笛卡尔树上的节点,其对应区间的上面最多只会有一颗星星。考虑树形 dp,设 $f_{i,j}$ 表示在横坐标为 $i$ 的节点对应区间的上面,星星的纵坐标为 $j$ 时的最大权值和。

直接 dp 状态数有 $\mathcal O(n^2)$ 个,但有效的状态只和星星数有关。

因此用 map 记录有效状态,转移时启发式合并即可,时间复杂度 $\mathcal O(n \log^2 n)$。

const int N = 2e5 + 7;
int n, m, a[N], s[N], t, l[N], r[N], rt;
map<int, ll> f[N];
ll g[N], z[N], ans, sum;

inline void work(int x, int y) {
    while (f[x].size() && f[x].begin() -> fi <= y)
        g[x] = max(g[x], f[x].begin() -> se + z[x]), f[x].erase(f[x].begin());
}

inline int merge(int x, int y) {
    if (f[x].size() < f[y].size()) swap(x, y);
    z[x] += g[y], z[y] += g[x], g[x] += g[y];
    for (auto o : f[y]) f[x][o.fi] = max(f[x][o.fi], o.se + z[y] - z[x]);
    return x;
}

int dfs(int x) {
    int y = x;
    if (l[x]) l[x] = dfs(l[x]), work(l[x], a[x]), y = merge(y, l[x]);
    if (r[x]) r[x] = dfs(r[x]), work(r[x], a[x]), y = merge(y, r[x]);
    return y;
}

int main() {
    rd(n);
    for (int i = 1; i <= n; i++) {
        rd(a[i]);
        int k = t;
        while (k && a[s[k]] < a[i]) --k;
        if (k) r[s[k]] = i;
        if (k < t) l[i] = s[k+1];
        s[++k] = i, t = k;
    }
    rt = s[1];
    rd(m);
    for (int i = 1, x, y, z; i <= m; i++)
        rd(x), rd(y), rd(z), f[x][y] = z, sum += z;
    rt = dfs(rt), ans = g[rt];
    for (auto o : f[rt]) ans = max(ans, o.se + z[rt]);
    print(sum - ans);
    return 0;
}

収穫 (Harvest)

题目太神仙了,先咕咕咕着。

迷い猫 (Stray Cat)

通信题,扔了。

Day4

首都 (Capital City)

答案相当于要找到一个包含颜色最少的连通块,满足连通块内的颜色和连通块外的颜色两两不同,我们称这样的连通块为合法连通块。

考虑点分治,对于每一次点分出来的重心和连通块,我们在连通块中找到最小的包含重心的合法连通块,用这个连通块的答案去更新答案。

这样,最优解一定会被更新到,时间复杂度 $\mathcal O(n \log n)$。

const int N = 2e5 + 7;
int n, m, a[N], c[N], ans, f[N], s[N], rt, mn;
vi e[N], b[N], p;
bool u[N], v[N], w[N];

void getrt(int x, int S) {
    v[x] = 1, s[x] = 1;
    if (!b[a[x]].size()) p.pb(a[x]);
    b[a[x]].pb(x); 
    int o = 0;
    for (int y : e[x])
        if (!v[y] && !w[y]) getrt(y, S), s[x] += s[y], o = max(o, s[y]);
    o = max(o, S - s[x]);
    if (o < mn) rt = x, mn = o;
    v[x] = 0;
}

void dfs(int x) {
    v[x] = 1, s[x] = 1;
    for (int y : e[x])
        if (!v[y] && !w[y]) f[y] = x, dfs(y), s[x] += s[y];
    v[x] = 0;
}

void solve(int x, int S) {
    rt = x, mn = S, f[x] = 0, getrt(x, S), dfs(rt);
    queue<int> q;
    q.push(a[rt]), u[a[rt]] = 1, v[rt] = 1;
    int cnt = 0;
    bool ok = (int)b[a[rt]].size() == c[a[rt]];
    while (ok && q.size()) {
        int x = q.front();
        q.pop();
        for (int y : b[x])
            while (!v[y]) {
                v[y] = 1;
                if (!u[a[y]]) {
                    if ((int)b[a[y]].size() != c[a[y]]) ok = 0;
                    q.push(a[y]), u[a[y]] = 1, ++cnt;
                }
                y = f[y];
            }
    }
    if (ok) ans = min(ans, cnt);
    for (int x : p) {
        for (int y : b[x]) v[y] = 0;
        b[x].clear(), u[x] = 0;
    }
    p.clear();
    w[rt] = 1;
    int now = rt;
    for (int nxt : e[now])
        if (!w[nxt]) solve(nxt, s[nxt]);
}

int main() {
    rd(n), rd(m), ans = m;
    for (int i = 1, x, y; i < n; i++)
        rd(x), rd(y), e[x].pb(y), e[y].pb(x);
    for (int i = 1; i <= n; i++) rd(a[i]), ++c[a[i]];
    solve(1, n), print(ans);
    return 0;
}

伝説の団子職人 (Legendary Dango Maker)(1,2,3,4,5,6

提答题,扔了。

治療計画 (Treatment Project)

我们以横坐标为时间轴,纵坐标为位置轴构建平面直角坐标系。

考虑一个治疗方案 $(T_i,L_i,R_i)$ 单独在这样一个坐标系中的覆盖区域,可以发现其覆盖的区域为以 $(T_i,L_i),(T_i,R_i)$ 为两个 $45^\circ$ 角向右的一个等腰直角三角形。

特别地,对于 $L_i = 1$ 的方案,它还能多覆盖一点三角形下方的区域;对于 $R_i = n$ 的方案,它还能多覆盖一点三角形上方的区域。具体可以自己手画一下。

由于最终要求所有人都被治愈,因此一个方案合法当且仅当这些区域可以拼接起来且使某个 $L_i=1$ 的方案与某个 $R_i = n$ 的方案连通

注意,这里的拼接并不是指有交,对于方案 $i,j$,$i$ 可以与 $j$ 单向地拼接当且仅当 $|T_i-T_j|\le R_i – L_j + 1$,具体也可以自己手画一下。

显然,求解这个问题就是一个最短路。

直接连边边数是 $\mathcal O(m^2)$ 的,显然无法承受,用线段树 + set 优化连边即可。

这个优化连边的 Trick 跟 P5471 [NOI2019]弹跳 很像。具体来说,在点权最短路上,每个点最多只会被松弛一次,即对于每个点,第一次更新到它时的值就是它的最短路(在边权最短路中这显然是不一定的)。于是我们可以在某一个点被更新过就立刻删掉它,这样时间复杂度就与边数无关,为 $\mathcal O(n \log n)$ 了。

这两道题都有一个共同点是,二维平面上从一个点到一个区域连边跑点权最短路,于是我们可以用一棵线段树维护第一维,线段树中每个节点用一个 set 存下在第一维里这个区间内的所有点,这样每个点被复制了 $\mathcal O(\log m)$ 次,总点数为 $\mathcal O(m \log m)$,跑上述所说的点权最短路,总时间复杂度为 $\mathcal O(m \log^2 m)$。

const int N = 1e5 + 7;
const ll inf = 1e18;
int n, m;
struct P {
    int t, l, r, c;
    inline void in() { rd(t), rd(l), rd(r), rd(c); }
    inline bool operator < (const P o) const { return t < o.t; }
} p[N];
struct T {
    int l, r;
    set<pi> s;
} t1[N<<2], t2[N<<2];
ll d[N], ans = inf;
pq<pair<ll, int>> q;
bool v[N], w[N];

void build(T* t, int p, int l, int r) {
    t[p].l = l, t[p].r = r;
    if (l == r) return;
    build(t, ls, l, md), build(t, rs, md + 1, r);
}

void ins(T *t, int p, int x, pi k) {
    t[p].s.insert(k);
    if (t[p].l == t[p].r) return;
    if (x <= md) ins(t, ls, x, k);
    if (x > md) ins(t, rs, x, k);
}

void upd(T *t, int p, int l, int r, int x, ll k) {
    if (l <= t[p].l && r >= t[p].r) {
        while (t[p].s.size() && t[p].s.begin() -> fi <= x) {
            int y = t[p].s.begin() -> se;
            t[p].s.erase(t[p].s.begin());
            if (w[y]) continue;
            d[y] = k + ::p[y].c, q.push(mp(-d[y], y)), w[y] = 1;
        }
        return;
    }
    if (l <= md) upd(t, ls, l, r, x, k);
    if (r > md) upd(t, rs, l, r, x, k);
}

int main() {
    rd(m), rd(n);
    for (int i = 1; i <= n; i++) p[i].in();
    sort(p + 1, p + n + 1);
    build(t1, 1, 1, n), build(t2, 1, 1, n);
    for (int i = 1; i <= n; i++)
        ins(t1, 1, i, mp(p[i].l - p[i].t, i)),
        ins(t2, 1, i, mp(p[i].l + p[i].t, i));
    for (int i = 1; i <= n; i++)
        if (p[i].l != 1) d[i] = inf;
        else d[i] = p[i].c, q.push(mp(-d[i], i)), w[i] = 1;
    while (q.size()) {
        int x = q.top().se;
        q.pop();
        if (v[x]) continue;
        v[x] = 1;
        if (x != 1) upd(t1, 1, 1, x - 1, p[x].r - p[x].t + 1, d[x]);
        if (x != n) upd(t2, 1, x + 1, n, p[x].r + p[x].t + 1, d[x]);
    }
    for (int i = 1; i <= n; i++)
        if (p[i].r == m) ans = min(ans, d[i]);
    print(ans == inf ? -1 : ans);
    return 0;
}

发表评论

电子邮件地址不会被公开。 必填项已用*标注