分类: 补题报告

Educational Codeforces Round 112 (Rated for Div. 2)

https://codeforces.com/contest/1555

C. Coin Rows

题意:
两行数,第一个人先走,拿走所有经过的;第二个人再走。只能往右下走,从左上出发。第一个人想让第二个拿的最少,第二个人要拿的最多,问第二个人拿多少

做法:
推一下发现是第二层的前缀和与第一层后缀和取max

拓展:
不妨考虑一下若不止两行该怎么做

D. Say No to Palindromes

题意:
给一个由abc三个字母组成的串,问至少改几个字母使得每个子串均不为长度大于1的回文。每次询问原串的一个子串

做法:
观察可知条件等价为连续三个字符各不相同,共六种情况,枚举即可,答案满足可加性

E. Boring Segments

题意:
数轴上给若干条线段,给出左右端点及其权值。当选取一条线段时,其覆盖的所有整点“连通”。现要求选定一个子集,使全部整点连通,同时使得最大权值减最小权值最小

做法:
首先,“连通”条件可进行转化:所有整点“连通”等价为所有整点及半整点 (x.5) 均被覆盖
由此,可以利用线段树对整体连通性进行维护:将点数翻倍(加上半整点),添加线段即区间 +1,移除即区间 -1,若全局最小值大于 0 则为连通
基于此,不难想到另一部分,将线段按权值从小到大排序,用 2-pointer 就能 O(n) 枚举所有情况
总复杂度 O(nlogn)

AC代码

#include <algorithm>
#include <iostream>

using namespace std;

const int MAX_N = 3E5 + 100;
const int MAX_M = 2E6 + 100;
const int INF = 1E6 + 100;

class SegmentTree {
   private:
    int n;
    int a[MAX_M << 2], lz[MAX_M << 2];

    void modify(int n, int l, int r, int ll, int rr, int x) {
        if (ll <= l && r <= rr) {
            a[n] += x;
            lz[n] += x;
            return;
        }

        int mid = (l + r) >> 1;
        if (lz[n]) {
            modify(n << 1, l, mid, l, mid, lz[n]);
            modify(n << 1 | 1, mid + 1, r, mid + 1, r, lz[n]);
            lz[n] = 0;
        }

        if (ll <= mid) modify(n << 1, l, mid, ll, rr, x);
        if (mid < rr) modify(n << 1 | 1, mid + 1, r, ll, rr, x);
        a[n] = min(a[n << 1], a[n << 1 | 1]);
    }

   public:
    void init(int _n) { n = _n; }

    void modify(int l, int r, int x) { modify(1, 1, n, l, r, x); }

    bool query() { return a[1]; }
};

class Segment {
   public:
    int l, r, w;
    bool operator<(const Segment& s) { return w < s.w; }
};

int n, m;
SegmentTree tr;
Segment seg[MAX_N];

int main() {
    ios::sync_with_stdio(false);
    cin >> n >> m;
    for (int i = 0; i < n; ++i) {
        cin >> seg[i].l >> seg[i].r >> seg[i].w;
        seg[i].l = seg[i].l * 2 - 1;
        seg[i].r = seg[i].r * 2 - 1;
    }
    sort(seg, seg + n);
    tr.init(2 * m - 1);
    // init

    int ans = INF;
    int l = 0, r = -1;
    while (!tr.query()) {
        r += 1;
        tr.modify(seg[r].l, seg[r].r, 1);
    }

    while (l < n) {
        if (tr.query()) ans = min(ans, seg[r].w - seg[l].w);
        tr.modify(seg[l].l, seg[l].r, -1);
        l += 1;
        while (r + 1 < n && !tr.query()) {
            r += 1;
            tr.modify(seg[r].l, seg[r].r, 1);
        }
    }

    cout << ans << endl;

    return 0;
}

心路历程:
折磨了一个多小时没想出来,最后看的题解
2-pointer倒不难想,维护这样的权值模式排序后这么扫比较常见
维护是完全没想到,一直在考虑dp线段树优化,最关键的第一步转化还是第一次见,是一个好技巧

F. Good Graph

题意:
动态维护一个图,只有加边操作。需要满足加边后任一简单环上异或和为 1,否则添加失败

做法:

首先观察出下面几个性质

  1. 若添加前两端点不连通,则添加成功
  2. 若添加的两端点在一棵树上,则路径异或和为 1 时添加成功
  3. 若添加的两点已经在一个简单环上,添加必失败
  4. 若添加的两端点间原路径有任一段包含在任一环上,添加必失败

后两条性质整合一下就是当添加的两端点间的路径经过一条环的边的话,必失败
然后就结束了,这显然用LCT维护就好了
需要注意的是IO量比较大,需要快读

AC代码

#include <cstdio>
#include <utility>
using namespace std;

const int MAX_N = 3E5 + 100;

class Node {
   public:
    int val, tot;
    bool isEdge;
    int eSz;
    int flag, flz, fTot;
    bool flip;
    Node *c[2], *p, *pp;

    void fix() {
        tot = val;
        fTot = flag;
        eSz = isEdge;
        if (c[0]) {
            c[0]->p = this;
            tot ^= c[0]->tot;
            fTot += c[0]->fTot;
            eSz += c[0]->eSz;
        }

        if (c[1]) {
            c[1]->p = this;
            tot ^= c[1]->tot;
            fTot += c[1]->fTot;
            eSz += c[1]->eSz;
        }
    }

    void pushFlip() {
        if (flz) {
            flz = 0;
            flag = isEdge;
            if (c[0]) {
                c[0]->flz = 1;
                c[0]->fTot = c[0]->eSz;
            }
            if (c[1]) {
                c[1]->flz = 1;
                c[1]->fTot = c[1]->eSz;
            }
        }

        if (!flip) return;
        flip = 0;
        swap(c[0], c[1]);
        if (c[0]) c[0]->flip ^= 1;
        if (c[1]) c[1]->flip ^= 1;
    }

    int type() { return p ? this == p->c[1] : 2; }

    void rot(int i, int j) {
        bool b = i ^ j;
        Node *x = c[i], *y = 2 == j ? x : x->c[j], *z = b ? y : x;

        if (y->p = p) p->c[type()] = y;
        c[i] = z->c[i ^ 1];
        if (j < 2) {
            x->c[j] = y->c[j ^ 1];
            z->c[j ^ 1] = b ? x : this;
        }
        y->c[i ^ 1] = b ? this : x;

        fix();
        x->fix();
        y->fix();

        swap(pp, y->pp);
    }

    void splay() {
        for (pushFlip(); p;) {
            if (p->p) p->p->pushFlip();
            p->pushFlip();
            pushFlip();

            if (2 == p->type())
                p->rot(type(), 2);
            else
                p->p->rot(p->type(), type());
        }
    }

    Node *first() {
        pushFlip();
        return c[0] ? c[0]->first() : (splay(), this);
    }
};

class LCT {
   public:
    Node node[MAX_N << 1];

    Node *access(Node *u) {
        for (u->splay(); Node *pp = u->pp; u = pp) {
            pp->splay();
            u->pp = 0;

            if (pp->c[1]) {
                pp->c[1]->p = 0;
                pp->c[1]->pp = pp;
            }
            pp->c[1] = u;
            pp->fix();
        }
        return u;
    }

    void makeRoot(Node *u) {
        access(u);
        u->splay();
        if (u->c[0]) {
            u->c[0]->flip ^= 1;
            u->c[0]->p = 0;
            u->c[0]->pp = u;
            u->c[0] = 0;
            u->fix();
        }
    }

    bool connected(Node *u, Node *v) {
        Node *ru = access(u)->first();
        return ru == access(v)->first();
    }

    void link(Node *u, Node *v) {
        if (connected(u, v)) return;
        makeRoot(u);
        u->pp = v;
    }

    void cut(Node *u, Node *v) {
        makeRoot(v);
        u->splay();
        if (v != (u->pp ?: u->c[0])) return;
        if (u->pp) {
            u->pp = 0;
        } else {
            u->c[0] = v->p = 0;
            u->fix();
        }
    }

    //====================

    Node *access(int u) { return access(node + u); }
    void makeRoot(int u) { makeRoot(node + u); }
    bool connected(int u, int v) { return connected(node + u, node + v); }
    void link(int u, int v) { link(node + u, node + v); }
    void cut(int u, int v) { cut(node + u, node + v); }
};

int read() {
    int ret = 0;
    char c = getchar();
    while ('0' > c || c > '9') c = getchar();
    while ('0' <= c && c <= '9') {
        ret *= 10;
        ret += c - '0';
        c = getchar();
    }
    return ret;
}

int n, q, m;
int a[MAX_N];
LCT tr;

int main() {
    n = read();
    q = read();
    for (int i = 1; i <= n; ++i) {
        a[i] = i;
    }
    m = n;

    while (q--) {
        int u = read(), v = read(), w = read();
        if (!tr.connected(u, v)) {
            puts("YES");

            m += 1;
            tr.node[m].isEdge = 1;
            tr.node[m].val = w;
            tr.link(u, m);
            tr.link(v, m);
        } else {
            tr.makeRoot(u);
            tr.access(v);
            tr.node[v].splay();

            if (tr.node[v].fTot -
                (tr.node[v].c[1] ? tr.node[v].c[1]->fTot : 0)) {
                puts("NO");
            } else if (1 ==
                       (tr.node[v].tot ^
                        (tr.node[v].c[1] ? tr.node[v].c[1]->tot : 0) ^ w)) {
                puts("YES");
                tr.node[v].c[0]->flz = 1;
                tr.node[v].c[0]->fTot = tr.node[v].c[0]->eSz;
            } else {
                puts("NO");
            }
        }
    }

    return 0;
}

心路历程
想出性质的时候还不会LCT,当时考虑的做法是缩点处理后两个性质
然后写了奇怪的LCT,维护了很多东西,常数巨大,T了
苦思之后在梦里想起有快读这么个东西,赶紧爬起来改了,就过了

Educational Codeforces Round 111 (Rated for Div. 2)

https://codeforces.com/contest/1550

D. Excellent Arrays

题意
要求构造一个数列 a ,使得 a_i + a_j = i + ja_i \neq i
给定数列长度 n,元素上下界 l, r
求数列的种类数模 10^9 + 7

数据范围
2\leq n \leq 2\sdot 10^5, -10^9 \leq l \leq 1, n \leq r \leq 10^9
多组数据保证 n 的和不超过上界

做法
首先很容易发现,数列一定是由 i - xi + x 一半一半构成的(奇数有一个可任选)
再看复杂度,很显然,枚举一个量级为 n 的东西即可
进一步地,发现 l, r, x 要满足下列关系
不妨考虑数列分为三段:分界点为最靠前的- x最靠后的+ x,不妨记为ll, rr
那么有关系 ll - x \geq l, rr + x \leq r,即 rr \leq r - x, ll \geq l + x
即给定 x,大于 r - xi 必须全取 i - x, 小于 l + xi 必须全取 i + x,剩下的可任意取
注意到只有当 r - x < nl + x > 1 的时候会受上下界影响,且不能过半,只用枚举一个 O(n) 的范围即可

AC代码

#include <iostream>
using namespace std;

#define int long long

const int MAX_N = 2E5 + 100;
const int MOD = 1E9 + 7;

int bin(int x, int n) {
    int ret = 1;
    for (; n; n >>= 1, x = x * x % MOD)
        if (n & 1) ret = ret * x % MOD;
    return ret;
}

int fac[MAX_N], facInv[MAX_N];
void initInv(int n) {
    fac[0] = 1;
    for (int i = 1; i <= n; ++i) fac[i] = fac[i - 1] * i % MOD;
    for (int i = 0; i <= n; ++i) facInv[i] = bin(fac[i], MOD - 2);
}

// 0 <= n <= m
inline int C(int n, int m) {
    return fac[m] * facInv[n] % MOD * facInv[m - n] % MOD;
}

int T;
int n, l, r;

signed main() {
    initInv(2E5);

    ios::sync_with_stdio(false);
    cin >> T;
    while (T--) {
        cin >> n >> l >> r;
        int ans = 0;
        int ll = min(r - n, 1 - l);
        int rr = ll + (n + 1) / 2;

        if (n & 1) {
            ans += 2 * max(0ll, ll - 1) * C(n / 2, n) % MOD;
            for (int x = max(1ll, ll); x <= rr; ++x) {
                int rp = max(0ll, n - r + x);
                int lp = max(0ll, l + x - 1);
                if (lp <= n / 2 && rp <= n / 2 + 1) {
                    ans = (ans + C(n / 2 - lp, n - lp - rp)) % MOD;
                }
                if (lp <= n / 2 + 1 && rp <= n / 2) {
                    ans = (ans + C(n / 2 + 1 - lp, n - lp - rp)) % MOD;
                }
            }
        } else {
            ans += max(0ll, ll - 1) * C(n / 2, n) % MOD;
            for (int x = max(1ll, ll); x <= rr; ++x) {
                int rp = max(0ll, n - r + x);
                int lp = max(0ll, l + x - 1);
                if (lp <= n / 2 && rp <= n / 2) {
                    ans = (ans + C(n / 2 - lp, n - lp - rp)) % MOD;
                }
            }
        }

        cout << ans << '\n';
    }

    return 0;
}

心路历程
最开始考虑枚举 ll,rr,利用式子 1 \leq x \leq Min\{ll - l, r - rr\} 确定 x
发现做不了,枚举 x 更方便,遂改邪归正

E. Stringforces

题意
给一个长度为 n 的包含前 k 个小写字母与 ? 的字符串
要求将 ? 替换为小写字母,使得以每个小写字母为全部元素的最长连续子串长度的最小值最大
例如串 aaababbbb a的长为3,b的长为4,最小值为3

数据范围
1\leq n \leq 2 \sdot 10^5, 1\leq k \leq 17

做法
挺明显的二分答案,关键在于如何check
因为 k 给的很小,所以check往 O(2^k) 上考虑,但是在那之前首先考虑更小的做法
题意转化为,给定一个长度 m,要求对于每种字母都构造出至少一段这样长度的子串
假如给定这些子串的排列顺序,那么贪心可以 O(k + n) 解决
即只要使每个子串尽量靠左即可,具体做法为dp记录每种元素的最靠左的完成子串位置即可
但是枚举排列顺序需要 O(k!) 显然不行
不过显然这 k! 种排列大量重叠,具有相同前缀
很容易想到优化成 O(2^k) 的做法,类似dfs弄一下,2^k 状态,O(k) 转移,再 O(kn) 预处理
check总复杂度为 O(k2^k + kn)

AC代码

#include <iostream>
#include <queue>
using namespace std;

const int MAX_N = 2E5 + 100;
const int MAX_K = 20;

int n, k;
char s[MAX_N];
int a[MAX_N], dp[MAX_N];
int b[MAX_N][MAX_K];

bool check(int m) {
    static int tmp[MAX_K];
    for (int i = 1; i <= k; ++i) {
        tmp[i] = -1;
        b[n + 1][i] = n + 1;
    }
    for (int i = n; i >= 1; --i) {
        if (a[i]) {
            for (int j = 1; j <= k; ++j) {
                if (j != a[i]) tmp[j] = -1;
            }
            if (-1 == tmp[a[i]]) tmp[a[i]] = i;
        } else {
            for (int j = 1; j <= k; ++j) {
                if (-1 == tmp[j]) tmp[j] = i;
            }
        }

        for (int j = 1; j <= k; ++j) {
            if (tmp[j] - i + 1 >= m) {
                b[i][j] = i + m - 1;
            } else {
                b[i][j] = b[i + 1][j];
            }
        }
    }
    // init

    for (int i = 0; i <= (1 << k) - 1; ++i) {
        dp[i] = n + 2;
    }
    queue<int> que;
    que.push(0);
    dp[0] = 1;
    while (!que.empty()) {
        int u = que.front();
        que.pop();

        for (int i = 1, j = 1; j <= k; i <<= 1, j += 1) {
            if (~u & i) {
                int v = u | i;
                int w = b[dp[u]][j];
                if (w <= n) {
                    if (n + 2 == dp[v]) que.push(v);
                    dp[v] = min(dp[v], w + 1);
                }
            }
        }
    }

    return dp[(1 << k) - 1] <= n + 1;
}

int main() {
    ios::sync_with_stdio(false);
    cin >> n >> k;
    cin >> (s + 1);
    for (int i = 1; i <= n; ++i) {
        a[i] = '?' == s[i] ? 0 : s[i] - 'a' + 1;
    }
    a[n + 1] = -1;
    // init

    int l = 1, r = n;
    while (l < r) {
        int mid = (l + r + 1) >> 1;
        if (check(mid)) {
            l = mid;
        } else {
            r = mid - 1;
        }
    }
    if (!check(r)) r -= 1;

    cout << r << endl;

    return 0;
}

心路历程
自己想的时候只想到二分答案和 2^k,之后就懵逼了
最后看了题解才捋清了思路
转移的时候忘记规划取min以及多次入队了,WA一发MLE一发,数据似乎挺水的

Codeforces Round 727 (Div. 2)

https://codeforces.com/contest/1539

E. Game with Cards

题意:
n 个回合游戏
初始时左右手各有一张值为 的卡片
每个回合会提供一张值为 k_i 的卡片,必须将其与左手或右手的卡片交换
要求每个回合后满足左右手的卡片值分别落在指定区间内
a_{l,i} \leq l \leq b_{l, i}, a_{r,i} \leq r \leq b_{r,i}

数据范围:
2 \leq n \leq 10 ^ 5
2 \leq m \leq 10^9
0 \leq k_i \leq m
0 \leq a_{l,i} \leq b_{l,i} \leq m,\ 0 \leq a_{r,i} \leq b_{r,i} \leq m

做法:
考虑每次交换对未来产生的影响,即其在未来能继续满足多少回合的区间
容易发现,这个“影响”越远越好
那么可以用dp来维护这个影响
f[i][0/1] 代表进行第 i 个回合,交换左/右手后,另一只手上的数字的最远影响范围
若记 g[i][0/1] 代表 k[i] 在左/右手时,满足 [i, g[i][0/1]] 的条件,而不满足 [i, g[i][0/1] + 1] 的条件
意会一下就能构造出转移方程,即满足条件且最大化 f[i][0/1]
那么考虑怎么获得 g[i][0/1]
即考虑这样一个问题:
给定一任意数组 a ,给 n 次询问,每次给出值 p ,求满足 a_i \geq p 的最小下标
挺显然可以用权值线段树离线维护,从右往左扫一遍
总复杂度 O(n + nlogn)

AC代码:

#include <iostream>
using namespace std;

#define int long long

const int MAX_N = 1E5 + 100;
const int INF = MAX_N;

class SegmentTree {
   private:
    int n, m;
    class Node {
       public:
        int a = INF, l, r;
    } a[20 * MAX_N];

    void modify(int n, int l, int r, int p, int x) {
        if (l == r) {
            a[n].a = min(a[n].a, x);
            return;
        }

        int mid = (l + r) >> 1;
        if (p <= mid) {
            if (!a[n].l) a[n].l = ++m;
            modify(a[n].l, l, mid, p, x);
        } else {
            if (!a[n].r) a[n].r = ++m;
            modify(a[n].r, mid + 1, r, p, x);
        }

        a[n].a = INF;
        if (a[n].l) a[n].a = min(a[n].a, a[a[n].l].a);
        if (a[n].r) a[n].a = min(a[n].a, a[a[n].r].a);
    }

    int query(int n, int l, int r, int ll, int rr) {
        if (0 == n) return INF;
        if (ll <= l && r <= rr) {
            return a[n].a;
        }

        int mid = (l + r) >> 1;
        int ret = INF;
        if (ll <= mid) ret = min(ret, query(a[n].l, l, mid, ll, rr));
        if (mid < rr) ret = min(ret, query(a[n].r, mid + 1, r, ll, rr));
        return ret;
    }

   public:
    void init(int _n) {
        n = _n;
        m = 1;
    }

    void modify(int p, int x) { modify(1, 1, n, p + 1, x); }

    int query(int l, int r) { return query(1, 1, n, l + 1, r + 1); }
};

int n, m;
int k[MAX_N];
int a[MAX_N][2];
int lt[MAX_N][2], rt[MAX_N][2];
int ans[MAX_N];

SegmentTree trr[2], trl[2];

int dp[MAX_N][2];
int lst[MAX_N][2];

signed main() {
    ios::sync_with_stdio(false);
    cin >> n >> m;
    trl[0].init(m + 1);
    trl[1].init(m + 1);
    trr[0].init(m + 1);
    trr[1].init(m + 1);
    for (int i = 1; i <= n; ++i) {
        cin >> k[i];
        cin >> lt[i][0] >> rt[i][0];
        cin >> lt[i][1] >> rt[i][1];
    }

    for (int i = n; i >= 1; --i) {
        trl[0].modify(lt[i][0], i);
        trr[0].modify(rt[i][0], i);
        trl[1].modify(lt[i][1], i);
        trr[1].modify(rt[i][1], i);

        a[i][0] = min(trl[0].query(k[i] + 1, m), trr[0].query(0, k[i] - 1)) - 1;
        a[i][1] = min(trl[1].query(k[i] + 1, m), trr[1].query(0, k[i] - 1)) - 1;
    }
    a[0][0] = trl[0].query(1, m) - 1;
    a[0][1] = trl[1].query(1, m) - 1;
    dp[0][0] = a[0][1];
    dp[0][1] = a[0][0];

    for (int i = 1; i <= n; ++i) {
        if (a[i][0] >= i) {
            if (-1 == dp[i - 1][1] || dp[i - 1][0] >= a[i - 1][1]) {
                dp[i][0] = dp[i - 1][0];
                lst[i][0] = 0;
            } else {
                dp[i][0] = a[i - 1][1];
                lst[i][0] = 1;
            }

            if (dp[i][0] < i) dp[i][0] = -1;
        } else {
            dp[i][0] = -1;
        }

        if (a[i][1] >= i) {
            if (-1 == dp[i - 1][0] || dp[i - 1][1] >= a[i - 1][0]) {
                dp[i][1] = dp[i - 1][1];
                lst[i][1] = 1;
            } else {
                dp[i][1] = a[i - 1][0];
                lst[i][1] = 0;
            }

            if (dp[i][1] < i) dp[i][1] = -1;
        } else {
            dp[i][1] = -1;
        }
    }

    if (-1 != dp[n][0]) {
        cout << "Yes" << endl;

        ans[n + 1] = 0;
        for (int i = n; i >= 2; --i) {
            ans[i] = lst[i][ans[i + 1]];
        }
        for (int i = 2; i <= n + 1; ++i) {
            cout << ans[i] << ' ';
        }
        cout << endl;
    } else if (-1 != dp[n][1]) {
        cout << "Yes" << endl;

        ans[n + 1] = 1;
        for (int i = n; i >= 2; --i) {
            ans[i] = lst[i][ans[i + 1]];
        }
        for (int i = 2; i <= n + 1; ++i) {
            cout << ans[i] << ' ';
        }
        cout << endl;
    } else {
        cout << "No" << endl;
    }

    return 0;
}

心路历程:
思路想了一会,但是显然完全没想清楚
线段树维护的东西一直在修修补补(主要是端点值),dp方程也一直在改
小错误相当多,乱WA了20发才过
另外还读错题意了

Bob holds one card with number 0 in the left hand and one in the right hand

这一句我直接用翻译拿到的是,左手是0右手是1
留着以后下饭用

F. Strange Array

题意:
给一个数组,对于每个数要求计算一个“奇怪值”,定义如下
任选一个子区间,排序后(相等值任意调换),每个值到中心位置(偶数取右侧)的距离将对奇怪值产生贡献
一个数的奇怪值就是它的贡献的最大值

数据范围:
1 \leq n \leq 2·10^5,\ 1\leq a_i \leq n

做法:
子区间排序后当前数距中位数的距离,直接维护显然困难
考虑换一种角度想这个东西
记子区间内大于当前数的数量有 p, 小于当前数的数量有 q
则这一距离可以转化为一个用 p, q 表示的式子,大致是 abs(p - q)/2,再上下取整弄一下
问题转化为了对于每个数,寻找子区间来最大化与最小化 p - q
更进一步的,维护的区间可以从待询问的数那里拆成左右两半分别维护,最后加起来即可
这个东西可以用线段树离线维护
考虑从小到大依次加入数字,那么未加入的都是比当前值大的
考虑一个数组 c 来记录这个关系,若 a_i < a_{cur} 这一位取-1,大于则为 1
那么一个区间的 p - q 即为 \Sigma c_i
最后考虑一下等于的情况该怎么处理就好了

AC代码:

#include <iostream>
#include <map>
#include <vector>
using namespace std;

const int MAX_N = 2E5 + 100;
const int INF = MAX_N;

class SegmentTree1 {
   private:
    int n;
    int a[MAX_N << 2];
    int lz[MAX_N << 2];

    void modify(int n, int l, int r, int ll, int rr, int x) {
        if (ll <= l && r <= rr) {
            a[n] += x;
            lz[n] += x;
            return;
        }

        int mid = (l + r) >> 1;
        if (lz[n]) {
            modify(n << 1, l, mid, l, mid, lz[n]);
            modify(n << 1 | 1, mid + 1, r, mid + 1, r, lz[n]);
            lz[n] = 0;
        }

        if (ll <= mid) modify(n << 1, l, mid, ll, rr, x);
        if (mid < rr) modify(n << 1 | 1, mid + 1, r, ll, rr, x);
        a[n] = max(a[n << 1], a[n << 1 | 1]);
    }

    int query(int n, int l, int r, int ll, int rr) {
        if (ll <= l && r <= rr) {
            return a[n];
        }

        int mid = (l + r) >> 1;
        if (lz[n]) {
            modify(n << 1, l, mid, l, mid, lz[n]);
            modify(n << 1 | 1, mid + 1, r, mid + 1, r, lz[n]);
            lz[n] = 0;
        }

        int ret = -INF;
        if (ll <= mid) ret = max(ret, query(n << 1, l, mid, ll, rr));
        if (mid < rr) ret = max(ret, query(n << 1 | 1, mid + 1, r, ll, rr));
        return ret;
    }

   public:
    void init(int _n) { n = _n; }

    void modify(int l, int r, int x) { modify(1, 1, n, l, r, x); }

    int query(int l, int r) { return query(1, 1, n, l, r); }
};

class SegmentTree2 {
   private:
    int n;
    int a[MAX_N << 2];
    int lz[MAX_N << 2];

    void modify(int n, int l, int r, int ll, int rr, int x) {
        if (ll <= l && r <= rr) {
            a[n] += x;
            lz[n] += x;
            return;
        }

        int mid = (l + r) >> 1;
        if (lz[n]) {
            modify(n << 1, l, mid, l, mid, lz[n]);
            modify(n << 1 | 1, mid + 1, r, mid + 1, r, lz[n]);
            lz[n] = 0;
        }

        if (ll <= mid) modify(n << 1, l, mid, ll, rr, x);
        if (mid < rr) modify(n << 1 | 1, mid + 1, r, ll, rr, x);
        a[n] = min(a[n << 1], a[n << 1 | 1]);
    }

    int query(int n, int l, int r, int ll, int rr) {
        if (ll <= l && r <= rr) {
            return a[n];
        }

        int mid = (l + r) >> 1;
        if (lz[n]) {
            modify(n << 1, l, mid, l, mid, lz[n]);
            modify(n << 1 | 1, mid + 1, r, mid + 1, r, lz[n]);
            lz[n] = 0;
        }

        int ret = INF;
        if (ll <= mid) ret = min(ret, query(n << 1, l, mid, ll, rr));
        if (mid < rr) ret = min(ret, query(n << 1 | 1, mid + 1, r, ll, rr));
        return ret;
    }

   public:
    void init(int _n) { n = _n; }

    void modify(int l, int r, int x) { modify(1, 1, n, l, r, x); }

    int query(int l, int r) { return query(1, 1, n, l, r); }
};

int n;
map<int, vector<int>> mp;
SegmentTree1 tr1, tr2;
SegmentTree2 tr3, tr4;
int ans[MAX_N];

int main() {
    ios::sync_with_stdio(false);
    cin >> n;
    tr1.init(n);
    tr2.init(n);
    tr3.init(n);
    tr4.init(n);
    for (int i = 1; i <= n; ++i) {
        int u;
        cin >> u;
        mp[u].push_back(i);

        tr1.modify(i, i, i);
        tr2.modify(i, i, n - i + 1);
        tr3.modify(i, i, i);
        tr4.modify(i, i, n - i + 1);
    }
    // init

    for (auto it : mp) {
        for (int i : it.second) {
            tr3.modify(i, n, -2);
            tr4.modify(1, i, -2);
        }

        for (int i : it.second) {
            int lA = tr2.query(1, i) - tr2.query(i, i);
            int rA = tr1.query(i, n) - tr1.query(i, i);
            int lB = tr4.query(1, i) - tr4.query(i, i);
            int rB = tr3.query(i, n) - tr3.query(i, i);
            ans[i] = max(abs((lA + rA + 1) / 2), abs((lB + rB) / 2));
        }

        for (int i : it.second) {
            tr1.modify(i, n, -2);
            tr2.modify(1, i, -2);
        }
    }

    for (int i = 1; i <= n; ++i) {
        cout << ans[i] << ' ';
    }
    cout << endl;

    return 0;
}

心路历程:
反应过来思路挺快的,写第一遍代码也挺快的
只是没想得太清楚,线段树没想够该用多少棵,等于的情况也只考虑了一半
调了蛮久的

2017-2018 Northwestern European Regional Contest (NWERC 2017)

https://codeforces.com/gym/101623

A. Ascending Photo

题意:
给一个数字串,需要花 x 刀将其切成 x + 1 个子段,满足任意排列子段后使整体有序,最小化 x 并输出

数据范围:
串长 1e6 ,元素 1 \leq h_i \leq 2e9

做法:
dp&map
反过来考虑,先全部切掉再组合,考虑怎么排可以少切一些
可以操作的空间只有相等的元素,两元素在原串与最终串中仍相邻则可少切一次,基于这个可以将原问题规约为dag考虑
dp很好考虑,从最终状态出发,f[i][j] 表示对于第 i 种元素,以其第 j 个结尾时的最小代价。转移方程如下
f[i][j] = {min}\{f[i - 1][p] + cal(i - 1, j)\}
cal 的作用大概就是算出 i 不以 j 开头时能否少切一次
总状态数为 O(n),转移搞搞可以弄成均摊 O(1) 的,总复杂度大概是 O(nlogn)
需要注意的是:相邻相同去重,离散化,单个元素的特殊情况

AC代码:

#include <iostream>
#include <map>
#include <vector>
using namespace std;

const int MAX_N = 1E6 + 100;
const int INF = MAX_N;

int n, m;
map<int, int> mp;
map<int, int> f[MAX_N];
vector<int> G[MAX_N];
int h[MAX_N];

void init() {
    cin >> n;
    for (int i = 1; i <= n; ++i) {
        cin >> h[i];
        mp[h[i]] = 0;
    }

    int oft = 0;
    for (int i = 2; i <= n; ++i) {
        if (h[i] == h[i - 1 - oft]) {
            oft += 1;
        }
        h[i - oft] = h[i];
    }
    n -= oft;
    //相邻去重

    int cnt = 0;
    for (auto& it : mp) {
        it.second = ++cnt;
    }
    for (int i = 1; i <= n; ++i) {
        h[i] = mp[h[i]];
        G[h[i]].push_back(i);
        m = max(m, h[i]);
    }
    //离散化,转为dag
}

int main() {
    ios::sync_with_stdio(false);
    init();

    f[0][0] = INF;
    f[0][n + 1] = 0;
    for (auto it : mp) {
        int i = it.second;

        int bs = INF;
        for (auto it : f[i - 1]) {
            bs = min(bs, it.second + (int)G[i].size());
        }
        //不连接时的最小值

        int mi = bs, mip = 0;  //最小值
        int si = bs, sip = 0;  //次小值
        for (int j : G[i]) {
            if (i - 1 == h[j - 1]) {
                if (si > f[i - 1][j - 1] + (int)G[i].size() - 1) {
                    si = f[i - 1][j - 1] + (int)G[i].size() - 1;
                    sip = j;

                    if (mi > si) {
                        swap(mi, si);
                        swap(mip, sip);
                    }
                }
            }
        }

        if (G[i].size() > 1) {
            for (int j : G[i]) {
                if (j == mip)
                    f[i][j] = si;
                else
                    f[i][j] = mi;
            }
        } else {
            f[i][G[i].back()] = mi;
        }
    }

    int ans = INF;
    for (auto it : f[m]) {
        ans = min(ans, it.second);
    }
    cout << ans - 1 << endl;

    return 0;
}

用例:

7
1 2 3 4 2 3 5
//3

7
1 2 3 4 2 4 5
//3

14
2 2 3 3 5 5 1 1 2 2 3 3 4 4
//3

心路历程:
训练的时候思路是从一个假贪心出发
规约着就变成了个链式约束的n-sat,然后越走越偏最后开始网络流乱搞(如果上课好好听讲,早知道这是个NP,就不至于这样了XD)
其实当时也想过dp,转移都想的差不多了,但是复杂度算错了,主要是第二维不知道怎么想的直接当 $n$ 算了,主要没想到用map来存状态
其实不是难题,但是硬刚了5h,说明大脑还是不够升级,思路也不够广
补的时候花了1h才调好,码力也不够XD

F. Factor-Free Tree

题意:
当且仅当一棵带权二叉树的所有节点满足:其权与其所有祖先的权互质,其为Factor-Free。现给一棵二叉树的前序遍历,构造一棵Factor-Free的树,若不可能,输出impossible
数据范围:
1 \leq n \leq 10^6,\ 1 \leq a_i \leq 10^7,时限3s

做法:
分解质因数,O(n \frac{\sqrt a}{loga} )
预处理每个数的最大互质区间 l[i], r[i]O(nloga) 空间 O(a)
对于区间 [ll, rr], 满足 ll \leq i \leq rr, l[i] \leq ll, rr \leq r[i]i 能作为这个区间的根,将区间分为 [ll, i - 1], [i + 1, rr],递归解决
可以证明,若有多个节点满足条件,可任取一个作为根
找根直接搜索即可,从两端往中间搜,找到即结束搜索
普通暴力容易想到卡成 O(n^2) 的数据,按照上述策略搜索则不会,复杂度不会证,猜测为 O(nlogn)
总复杂度 O(n \frac{\sqrt a}{loga} + nloga + nlogn)

AC代码:

#include <iostream>
using namespace std;

const int MAX_N = 1E6 + 100;
const int MAX_A = 1E7 + 100;
const int MAX_SQRT_A = 4E3 + 100;
const int MAX_LOG_A = 27;

int pSz, pr[MAX_SQRT_A];
void initPrime(int n) {
    static bool vis[MAX_SQRT_A];
    for (int i = 2; i <= n; ++i) {
        if (!vis[i]) pr[pSz++] = i;
        for (int j = 0; j < pSz; ++j) {
            if (pr[j] * i > n) break;
            vis[pr[j] * i] = true;
            if (0 == i % pr[j]) break;
        }
    }
}

int fSz, fac[MAX_LOG_A];
void getFac(int x) {
    fSz = 0;
    for (int j = 0; j < pSz; ++j) {
        if (x % pr[j]) continue;
        fac[fSz++] = pr[j];
        while (0 == x % pr[j]) {
            x /= pr[j];
        }
    }

    if (x > 1) {
        fac[fSz++] = x;
    }
}

int n;
int ans[MAX_N];

int p[MAX_A];
int l[MAX_N], r[MAX_N];

bool solve(int ll, int rr, int fat) {
    if (ll > rr) {
        return true;
    }
    if (ll == rr) {
        ans[ll] = fat;
        return true;
    }

    for (int i = ll, j = rr; i <= j; ++i, --j) {
        if (l[i] <= ll && rr <= r[i]) {
            ans[i] = fat;
            return solve(ll, i - 1, i) && solve(i + 1, rr, i);
        }

        if (l[j] <= ll && rr <= r[j]) {
            ans[j] = fat;
            return solve(ll, j - 1, j) && solve(j + 1, rr, j);
        }
    }

    return false;
}

int main() {
    initPrime(3200);

    ios::sync_with_stdio(false);
    cin >> n;
    for (int i = 1; i <= n; ++i) {
        int u;
        cin >> u;
        getFac(u);

        l[i] = 1;
        r[i] = n;
        for (int j = 0; j < fSz; ++j) {
            l[i] = max(l[i], p[fac[j]] + 1);
            r[p[fac[j]]] = min(r[p[fac[j]]], i - 1);
            p[fac[j]] = i;
        }
    }
    //初始化l, r

    if (solve(1, n, 0)) {
        for (int i = 1; i <= n; ++i) {
            cout << ans[i] << ' ';
        }
        cout << endl;
    } else {
        cout << "impossible" << endl;
    }

    return 0;
}

心路历程:
做法的前半大概10m就想到了,但是想不到基于l,r数组来找根的合适做法,一直觉得复杂度不对,考虑了一整天奇怪数据结构
最后突然来了灵感搜了题解,发现没人讲最后面这个怎么搞,发现不对劲,试着分析了一下复杂度发现可以不是 O(n^2)
做的时候当了点子王,犯了nt错误,贴在下面供以后娱乐

bool solve(int ll, int rr, int fat) {
    ...
    bool flag;  //利用堆空间未初始化的随机特性
    for (int i = ll, j = rr; i <= j;) {
        if (flag) {
            ...
        } else {
            ...
        }
        flag = !flag;
    }
    ...
}

测试之后发现,只要初始化flag就能过
对这个问题又做了进一步测试,代码如下

#include <iostream>
using namespace std;

bool go(int i) {
    bool flag;
    for (int j = 0; j < i; ++j)
    flag = !flag;
    return flag;
}

int main() {
    ios::sync_with_stdio(false);

    bool t;
    for (int i = 0; i < 2E5; ++i) t ^= go(i);
    cout << t << endl;

    return 0;
}

用的是cf的评测机,并没有发现对效率有影响,而且即使没有初始化,也是为false的(没有广泛测试)
所以最后我猜是这个东西影响了O2优化,因为不显式初始化的话,编译器并不知道初始值

J. Juggling Troupe

题意:
给一个元素为0,1,2的串,其在每个时刻会发生如下变化
假如一个位置(1\leq i \leq n)上的 a_i \geq 2 ,将会发生:a_i =a_i-2, a_{i-1} = a_{i-1} +1, a_{i + 1} = a_{i + 1} + 1
注意边界会"吞掉"数
同一时刻所有的变化同时发生
问时间足够长后,串的内容为什么

数据范围:
1 \leq n \leq 10^6, 0 \leq a_i \leq 2

做法:
打个表,发现如下两个规律:

  1. 操作可拆分,例如 0012200 -> 0111101,可拆为0011200 -> 0101110 -> 0102110 -> 0111101
  2. 连续的一串1中一个2会在对称位置产生0,并且区间左右延长一格,例如 00112100 -> 01101110

之后链表维护一下就好了

AC代码:

#include <cstring>
#include <iostream>
#include <vector>
using namespace std;

const int MAX_N = 1E6 + 100;

int n;
char s[MAX_N];
vector<int> que;

int head, tail, m;
int l[MAX_N << 1], r[MAX_N << 1];
int lst[MAX_N << 1], nxt[MAX_N << 1];

int i;
void go(int p) {
    while (nxt[i] && p >= l[nxt[i]]) {
        i = nxt[i];
    }

    if (p > r[i]) {
        if (p == r[i] + 1 && p == l[nxt[i]] - 1) {
            r[i] = r[nxt[i]];
            if (nxt[nxt[i]]) {
                lst[nxt[nxt[i]]] = i;
            } else {
                tail = i;
            }
            nxt[i] = nxt[nxt[i]];

        } else if (p == r[i] + 1) {
            r[i] += 1;
        } else if (nxt[i] && p == l[nxt[i]] - 1) {
            l[nxt[i]] -= 1;
        } else {
            m += 1;
            l[m] = r[m] = p;
            if (lst[i]) {
                nxt[lst[i]] = m;
            } else {
                head = m;
            }
            lst[m] = lst[i];
            nxt[m] = i;
            lst[i] = m;
        }

        return;
    }

    int p0 = l[i] + r[i] - p;  //对称位置

    m += 1;
    l[m] = p0 + 1;
    r[m] = r[i] + 1;
    if (r[m] > n) r[m] = n;

    lst[m] = i;
    nxt[m] = nxt[i];
    if (nxt[i]) {
        lst[nxt[i]] = m;
    } else {
        tail = m;
    }
    nxt[i] = m;

    l[i] -= 1;
    if (l[i] <= 0) l[i] = 1;
    r[i] = p0 - 1;

    int lp = i, rp = m;
    if (lst[lp] && r[lst[lp]] + 1 == l[lp]) {
        r[lst[lp]] = r[lp];
        lst[nxt[lp]] = lst[lp];
        nxt[lst[lp]] = nxt[lp];
        i = lst[lp];
    }

    if (nxt[rp] && l[nxt[rp]] - 1 == r[rp]) {
        l[nxt[rp]] = l[rp];
        lst[nxt[rp]] = lst[rp];
        nxt[lst[rp]] = nxt[rp];
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin >> s;
    n = strlen(s);
    int ll = 1, rr = 0;
    for (int i = 1; i <= n; ++i) {
        if ('0' == s[i - 1]) {
            if (rr >= ll) {
                m += 1;
                l[m] = ll;
                r[m] = rr;
                lst[m] = m - 1;
                nxt[m - 1] = m;
            }
            ll = i + 1;
            rr = i;
        } else {
            if ('2' == s[i - 1]) {
                que.push_back(i);
            }
            rr += 1;
        }
    }
    if (rr >= ll) {
        m += 1;
        l[m] = ll;
        r[m] = rr;
        lst[m] = m - 1;
        nxt[m - 1] = m;
    }
    // init

    if (m) {
        head = 1;
        tail = m;
    } else {
        cout << s << endl;
        return 0;
    }

    i = head;
    for (int u : que) {
        go(u);
    }

    for (int i = 1; i < min(n + 1, l[head]); ++i) {
        cout << 0;
    }
    cout.flush();
    for (int i = head; i; i = nxt[i]) {
        for (int j = max(1, l[i]); j <= min(n, r[i]); ++j) {
            cout << 1;
        }
        cout.flush();
        if (nxt[i]) {
            for (int j = max(1, r[i] + 1); j < min(n + 1, l[nxt[i]]); ++j) {
                cout << 0;
            }
            cout.flush();
        } else {
            break;
        }
    }
    for (int i = max(1, r[tail] + 1); i <= n; ++i) {
        cout << 0;
    }
    cout.flush();

    return 0;
}

心路历程:
打表找到规律还是挺快的,弄了二十来分钟吧
写链表算上调试大概弄了2h,太爬了
证明目前不会,主要是第一条,从第一条证第二题还是挺简单的
暴力程序这里就不贴了

E. English Restaurant

这应该是个概率dp,状态不行,捋不清,先占坑