Codeforces Round #629 (Div. 3)

题目链接:https://codeforces.com/contest/1328

A - Divisibility Problem

很古老的一个idea,送分题。

B - K-th Beautiful String

题意:要求构造一个由 \(n-2\) 个'a'和 \(2\) 个'b'构成的字符串,显然这样的字符串有 \(C_{n}^{2}\) 种,找出其中字典序第 \(k\) 小的答案。

题解:线性枚举第一个'b'的位置,很快可以找到第 \(k\) 小需要的第一个'b'在哪,然后直接计算出第二个'b'在哪。假如不是要输出整个字符串的话,应该有更快的做法,比如log的做法:计算第一个'b'在倒数第 \(i\) 个的时候,有多少种串,这个显然和 \(n\) 无关,可以重复用,然后在前缀和上面二分。或者有个接近线性的做法:因为前面这个前缀和是个等差数列求和,必然是个二次形式的东西,可以通过解方程(开平方)求出 \(k\) 大概对应第一个'b'在哪个位置,然后在这个附近开始找就可以了。

C - Ternary XOR

这个东西这么难,学弟居然会。

题意:给一个长度为 \(n\) 的字符串 \(s\) ,只有'0','1','2'三种数字,保证最左侧一定是'2',求构造两个字符串 \(a\)\(b\) ,使得 \(a \oplus b\) 恰好为 \(s\) ,定义三进制的异或 \(\oplus\)\((a_i+b_i)\mod 3\) 。且他们代表的数字 \(max(a,b)\) 最小。

题解:一开始觉得直接把所有的'2'换成两个'1',把'1'换成'1'和'0',把'0'换成两个'0'。但是仔细想想假如有多个'1'的话,应该是第一个'1'是分成'1'和'0',剩下的'1'分成'0'和'1',然后又注意到假如前面出现了一个'1'之后,就必定存在一个大数和一个小数,这时'2'要分解为'0'和'2',也就是说,假如没有任何'1',那么对半分就是最好的结果。否则把第一个'1'分给 \(a\) ,然后 \(a\) 后面就可以全部接'0'了,因为这时候无论 \(b\) 接什么都不可能比 \(a\) 大。

题意:给一个环,环上的节点有一些数,求一种用的颜色数最少的染色方案,使得环上的这些数中,所有的相邻位置且数字不同的二元组,都染上不同的颜色。

题解:首先,假如所有的数字都相同,那么肯定只需要1种颜色。否则,若至少有两种数字,那么在数字切换的位置就至少需要2种颜色。然后,显然假如环的长度是偶数,最大的颜色数就是2种,那么染色方案"12121212"就是一种合法的解,因为相邻位置都不同颜色,根本不管是不是数字不同。根据这个构造,可以看出来,假如环的长度是奇数,最大的颜色就是3种,因为"121212123"就是一种合法的构造,因为相邻位置都不同颜色,根本不管是不是数字不同。那么会不会有2种颜色的方法呢?继续按照这个奇偶染色的思路走,因为是奇数长度,所以只需要让有一个连续位置染成同色即可,注意到染成同色的,必须是连续的相同数字,所以就找是否存在这一的连续相同数字,找到第一个就break出来,注意特殊处理:第一个就连续相同、首尾连续相同,两种比较特别的情况。

int a[200005];
int b[200005];
 
void TestCase() {
    int n;
    scanf("%d", &n);
    for(int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);
    a[n + 1] = a[1];
    bool allsame = 1;
    for(int i = 1; i <= n; ++i) {
        if(a[i] != a[i + 1]) {
            allsame = 0;
            break;
        }
    }
    if(allsame) {
        //没有连续两个元素是相同的,全部染1
        puts("1");
        for(int i = 1; i <= n; ++i)
            printf("%d%c", 1, " \n"[i == n]);
        return;
    }
    //至少有一个位置,是连续两个元素相同,至少是2
    if(n % 2 == 0) {
        //是偶数
        puts("2");
        for(int i = 1; i <= n; ++i)
            printf("%d%c", 2 - (i & 1), " \n"[i == n]);
        return;
    }
    //是奇数
    //假如有连续两个元素是相同的,就染成同色,然后剩下的依然是间隔
    int pos = -1;
    for(int i = 1; i <= n; ++i) {
        if(a[i] == a[i + 1]) {
            pos = i;
            break;
        }
    }
    if(pos == -1) {
        //不存在,至少需要3种
        puts("3");
        for(int i = 1; i <= n - 1; ++i)
            printf("%d ", 2 - (i & 1));
        puts("3");
        return;
    }
    //存在,则把这两个位置染成同色
    b[0] = 2;
    puts("2");
    if(pos != n) {
        for(int i = 1; i < pos; ++i)
            b[i] = 2 - (i & 1);
        b[pos] = b[pos + 1] = 3 - b[pos - 1];
        for(int i = pos + 2; i <= n; ++i)
            b[i] = 3 - b[i - 1];
        for(int i = 1; i <= n; ++i)
            printf("%d%c", b[i], " \n"[i == n]);
        return;
    } else {
        for(int i = 1; i <= n - 1; ++i)
            printf("%d ", 2 - (i & 1));
        puts("1");
        return;
    }
}

*E - Tree Queries

好假哦,我在干什么。

标签:树上倍增,LCA

题意:给一棵 \(n\) 个节点的树,然后若干次询问,第 \(i\) 次询问一个大小为 \(k_i\) 的点集 \(v_1,v_2,...,v_{k_i}\) ,问是否存在根节点到某个节点的路径,使得这个点集到路径的距离不是0就是1。所有询问的点集大小的总和与 \(n\) 同阶。

题解:首先想到一个贪心,就是选出的节点必定是叶子,但是总不能预处理每个叶子对应覆盖的点集吧?甚至连每个叶子对应的到根节点的路径都不能存下来。然后又发现,假如有若干个深度最大的节点,那么他们必须要有同一个父亲 \(p\) ,否则无解。经过这个观察之后,就知道一定要选根节点到 \(p\) 的路径,而且要求点集中的点都要在路径上,或者其父亲在路径上。再观察一下假如把根节点的父亲定义为自身,那么只需要所有点的父亲都在路径上,当时就写了一个预处理这个路径进入set,然后从中查询所有点的父亲的算法。TLE58了,当时还以为是哪里死循环了,仔细看了所有的循环都不觉得有问题,就觉得为什么nlogn的算法会不行,改成用个vis数组打标记来替代set。结果还真变快了,变成TLE76了,加个快读还是TLE76。

vector<int> G[200005];
int dep[200005];
int pa[200005];
 
void dfs(int u, int p) {
    pa[u] = p;
    if(u != p)
        dep[u] = dep[p] + 1;
    for(auto &v : G[u]) {
        if(v == p)
            continue;
        dfs(v, u);
    }
    return;
}
 
int v[200005];
set<int> SET;
 
void solve() {
    int k;
    scanf("%d", &k);
    int maxdep = 0;
    for(int i = 1; i <= k; ++i) {
        scanf("%d", &v[i]);
        if(dep[v[i]] > maxdep)
            maxdep = dep[v[i]];
    }
    int gp = -1;
    for(int i = 1; i <= k; ++i) {
        if(dep[v[i]] == maxdep) {
            if(gp == -1)
                gp = pa[v[i]];
            else {
                if(pa[v[i]] != gp) {
                    puts("NO");
                    return;
                }
            }
        }
    }
    SET.clear();
    SET.insert(1);
    while(gp != 1) {
        SET.insert(gp);
        gp = pa[gp];
    }
    for(int i = 1; i <= k; ++i) {
        if(SET.count(pa[v[i]]) == 0) {
            puts("NO");
            return;
        }
    }
    puts("YES");
    return;
}
 
void TestCase() {
    int n, m;
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; ++i)
        G[i].clear();
    for(int i = 1; i <= n - 1; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G[u].push_back(v);
        G[v].push_back(u);
    }
    dep[1] = 1;
    dfs(1, 1);
    while(m--)
        solve();
    return;
}
vector<int> G[200005];
int dep[200005];
int pa[200005];
 
void dfs(int u, int p) {
    pa[u] = p;
    if(u != p)
        dep[u] = dep[p] + 1;
    for(auto &v : G[u]) {
        if(v == p)
            continue;
        dfs(v, u);
    }
    return;
}
 
int v[200005];
int vis[200005];
 
int read() {
    int res = 0;
    char c = getchar();
    while(!isdigit(c))
        c = getchar();
    while(isdigit(c)) {
        res = res * 10 + c - '0';
        c = getchar();
    }
    return res;
}
 
void solve() {
    int k;
    k = read();
    int maxdep = 0;
    for(int i = 1; i <= k; ++i) {
        v[i] = read();
        if(dep[v[i]] > maxdep)
            maxdep = dep[v[i]];
    }
    int gp = -1;
    for(int i = 1; i <= k; ++i) {
        if(dep[v[i]] == maxdep) {
            if(gp == -1)
                gp = pa[v[i]];
            else {
                if(pa[v[i]] != gp) {
                    puts("NO");
                    return;
                }
            }
        }
    }
    int tgp = gp;
    vis[1] = 1;
    while(gp != 1) {
        vis[gp] = 1;
        gp = pa[gp];
    }
    bool suc = 1;
    for(int i = 1; i <= k; ++i) {
        if(!vis[pa[v[i]]]) {
            suc = 0;
            break;
        }
    }
    vis[1] = 0;
    while(tgp != 1) {
        vis[tgp] = 0;
        tgp = pa[tgp];
    }
    if(suc)
        puts("YES");
    else
        puts("NO");
    return;
}
 
void TestCase() {
    int n, m;
    n = read(), m = read();
    for(int i = 1; i <= n; ++i)
        G[i].clear();
    for(int i = 1; i <= n - 1; ++i) {
        int u, v;
        u = read(), v = read();
        G[u].push_back(v);
        G[v].push_back(u);
    }
    dep[1] = 1;
    dfs(1, 1);
    while(m--)
        solve();
    return;
}

TLE了3次之后,突然意识到,这样预处理整条链,单次的复杂度可以到 \(O(n)\) ,整个复杂度是 \(O(n^2)\) 的,看来我玄学优化加太多了导致这样都可以过75组数据。然后想了想,应该把这些点按深度排序,然后每次取相邻的两个点的父亲验证他们的祖先后代关系,假如不是祖先后代,就说明NO,否则就跳到那个祖先节点继续走,假如一直都是祖先,那么把这些祖先连成一条链就是答案。

vector<int> G[200005];
int dep[200005];
int fa[200005][20 + 1];
 
void dfs(int u, int p) {
    dep[u] = dep[p] + 1;
    fa[u][0] = p;
    for (int i = 1; i <= 20; i++)
        fa[u][i] = fa[fa[u][i - 1]][i - 1];
    for(auto &v : G[u]) {
        if(v == p)
            continue;
        dfs(v, u);
    }
}
 
int read() {
    int res = 0;
    char c = getchar();
    while(!isdigit(c))
        c = getchar();
    while(isdigit(c)) {
        res = res * 10 + c - '0';
        c = getchar();
    }
    return res;
}
 
pii v2[200005];
 
void solve() {
    int k;
    k = read();
    for(int i = 1; i <= k; ++i) {
        int x = read();
        v2[i] = {dep[x], x};
    }
    if(k == 1) {
        puts("YES");
        return;
    }
    sort(v2 + 1, v2 + 1 + k);
    for(int i = 2; i <= k; ++i) {
        if(v2[i].first == v2[i - 1].first) {
            if(fa[v2[i].second][0] != fa[v2[i - 1].second][0]) {
                puts("NO");
                return;
            }
        }
    }
    int cur = v2[k].second;
    for(int i = k - 1; i >= 1; --i) {
        int x = v2[i].second;
        for(int j = 20; j >= 0; --j) {
            if(dep[fa[cur][j]] >= dep[x])
                cur = fa[cur][j];
        }
        assert(dep[cur] == dep[x]);
        if(fa[cur][0] != fa[x][0]) {
            puts("NO");
            return;
        }
    }
    puts("YES");
    return;
}
 
void TestCase() {
    int n, m;
    n = read(), m = read();
    for(int i = 1; i <= n; ++i)
        G[i].clear();
    for(int i = 1; i <= n - 1; ++i) {
        int u, v;
        u = read(), v = read();
        G[u].push_back(v);
        G[v].push_back(u);
    }
    dfs(1, 0);
    while(m--)
        solve();
    return;
}

应该还有办法优化,可以根据深度差的信息找到要跳的log的上限(类似ST表的预处理)。但是实际跑起来却更慢。

补充:看了一下官方题解,学到一些新的套路:判断祖先后辈关系甚至不需要用log的树上倍增法,注意在dfs中打上in时间和out时间,祖先后辈关系的in和out时间必定是嵌套的,这个只需要扫一遍就可以得到答案。甚至连排序都不需要,只需要所有的in出现之后再出现out就可以了。

struct Edge {
    int v, nxt;
} edge[400005];
int head[200005], top;

int pa[200005];
int intime[200005];
int outtime[200005];
int t;

void dfs(int u, int p) {
    pa[u] = p;
    intime[u] = ++t;
    for(int i = head[u]; i; i = edge[i].nxt) {
        int v = edge[i].v;
        if(v == p)
            continue;
        dfs(v, u);
    }
    outtime[u] = ++t;
}

int read() {
    int res = 0;
    char c = getchar();
    while(!isdigit(c))
        c = getchar();
    while(isdigit(c)) {
        res = res * 10 + c - '0';
        c = getchar();
    }
    return res;
}

void solve() {
    int k;
    k = read();
    int maxin = -INF, minout = INF;
    for(int i = 1; i <= k; ++i) {
        int x = read();
        x = x == 1 ? x : pa[x];
        maxin = max(maxin, intime[x]);
        minout = min(minout, outtime[x]);
    }
    if(maxin < minout)
        puts("YES");
    else
        puts("NO");
    return;
}

void TestCase() {
    int n, m;
    n = read(), m = read();
    top = 0;
    for(int i = 1; i <= n - 1; ++i) {
        int u, v;
        u = read(), v = read();
        ++top;
        edge[top].v = v;
        edge[top].nxt = head[u];
        head[u] = top;
        ++top;
        edge[top].v = u;
        edge[top].nxt = head[v];
        head[v] = top;
    }
    t = 0;
    dfs(1, 0);
    while(m--)
        solve();
    return;
}

*F - Make k Equal

题意:给 \(n\) 个数,要求使用最少的操作,使得至少有 \(k\) 个数相同。每次操作:要么把某个等于最大值的数-1.要么把某个等于最小值的数+1。

题解:需要注意到一点,就是最后这些相同的数,总是可以在初始给的数字之中,这个和lyd的书上面给的情况一样。那么枚举每个值 \(v=a[i]\) 作为最后的相同的 \(k\) 个数的情况,首先特判掉本身就有 \(k\) 个数相同的值的情况,然后要注意到:

假如要把小于 \(v\) 的数的一部分变成 \(v\) ,首先要全部变成 \(v-1\) 。然后再选一部分变成 \(v\) ,而不是直接全部变成 \(v\)
假如要把大于 \(v\) 的数的一部分变成 \(v\) ,首先要全部变成 \(v+1\) 。然后再选一部分变成 \(v\) ,而不是直接全部变成 \(v\)

也就是说,增加 \(v\) 的个数的办法,要么途径 \(v-1\) ,要么途径 \(v+1\) ,或者两者都是。

假如是“两者都是”,就把其他的数全部变成 \(v-1\) 或者 \(v+1\) ,然后任选其中的 \(k-cnt[v]\) 个即可。

假如是“只有一侧”,先确认这一侧的数有至少 \(k\) 个,然后就把这一侧的数全部变成 \(v-1\) 或者 \(v+1\) ,然后任选其中的 \(k-cnt[v]\) 个即可。

小心乘法溢出,看清楚括号的结合顺序,或者干脆全部用ll就不会出事。

int a[200005];
int x[200005];
int cnt[200005];
int sumcnt[200005];
ll sum[200005];
ll pre[200005];
ll suf[200005];

void TestCase() {
    int n, k;
    scanf("%d%d", &n, &k);
    for(int i = 1; i <= n; ++i) {
        scanf("%d", &a[i]);
        x[i] = a[i];
    }
    sort(x + 1, x + 1 + n);
    int m = unique(x + 1, x + 1 + n) - (x + 1);
    for(int i = 1; i <= n; ++i) {
        int pos = lower_bound(x + 1, x + 1 + m, a[i]) - x;
        ++cnt[pos];
        if(cnt[pos] >= k) {
            puts("0");
            return;
        }
    }
    for(int i = 1; i <= m; ++i) {
        sumcnt[i] = sumcnt[i - 1] + cnt[i];
        sum[i] = sum[i - 1] + 1ll * cnt[i] * x[i];
    }
    for(int i = 1; i <= m; ++i) {
        pre[i] = 1ll * sumcnt[i - 1] * (x[i] - 1) - sum[i - 1];
        suf[i] = (sum[m] - sum[i]) - 1ll * (n - sumcnt[i]) * (x[i] + 1);
    }

    /*for(int i = 1; i <= m; ++i) {
        printf("i=%d x=%d cnt=%d\n", i, x[i], cnt[i]);
        printf("  sumcnt=%d sum=%lld\n", sumcnt[i], sum[i]);
        printf("  pre=%lld suf=%lld\n", pre[i], suf[i]);
    }*/

    ll ans = LINF;
    for(int i = 1; i <= m; ++i) {
        ll tmp1 = pre[i] + suf[i] + k - cnt[i];
        ans = min(ans, tmp1);
        if(sumcnt[i] >= k) {
            ll tmp2 = pre[i] + k - cnt[i];
            ans = min(ans, tmp2);
        }
        if(n - sumcnt[i - 1] >= k) {
            ll tmp3 = suf[i] + k - cnt[i];
            ans = min(ans, tmp3);
        }
        //printf("i=%d ans=%lld\n", i, ans);
    }
    printf("%lld\n", ans);
    return;
}

为什么unique不是把元素移动到末尾呢?

posted @ 2020-03-27 10:03  KisekiPurin2019  阅读(155)  评论(0编辑  收藏  举报