Codeforces Round #615 (Div. 3)

暂时没有F题的题解。太菜了。

A - Collecting Coins

题意:有三个人,分别有a,b,c枚硬币,你有n枚硬币,要求把n枚硬币全部给这三个人并且使得他们的硬币数变为相等。问是否可行。

题解:先验证硬币总数a+b+c+n是否能被3整除,然后验证要补的硬币的数量少于n。

void test_case() {
    int a[5], n;
    scanf("%d%d%d%d", &a[1], &a[2], &a[3], &n);
    sort(a + 1, a + 1 + 3);
    int sum = a[3] - a[1] + a[3] - a[2];
    if(sum > n || (sum - n) % 3 != 0)
        puts("NO");
    else
        puts("YES");
}

B - Collecting Packages

题意:有个机器人,只能向上走或者向右走,要从(0,0)经过所有的点(xi,yi),求是否可行,若可行则输出字典序最小的方案。

题解:字典序最小就是说先往右走再往上走。直接把所有点按x排序,要求每个点与(0,0)包围的矩形包含此前的所有点,只需要比前面的点都高就行了。由数学归纳法可知只需要比前一个点高就行了。

int n;
pii p[1005];
 
void test_case() {
    scanf("%d", &n);
    for(int i = 1; i <= n; ++i) {
        int x, y;
        scanf("%d%d", &x, &y);
        p[i] = {x, y};
    }
    sort(p + 1, p + 1 + n);
    for(int i = 2; i <= n; ++i) {
        if(p[i].second < p[i - 1].second) {
            puts("NO");
            return;
        }
    }
    puts("YES");
    int cx = 0, cy = 0;
    for(int i = 1; i <= n; ++i) {
        while(cx < p[i].first) {
            putchar('R');
            ++cx;
        }
        while(cy < p[i].second) {
            putchar('U');
            ++cy;
        }
    }
    putchar('
');
}

C - Product of Three Numbers

题意:给一个数n,满足n>=2,要求将n分解成互异的三个数a,b,c,使得a*b*c==n,且a,b,c>=2。给出一种分解的方案,或者说明其不存在。

题解:一开始没看见>=2直接判断质数就上了(虽然这样也不对,比如9不能分解为1,3,3),仔细一想跟质数的种类有关系,所以可以直接分解质因数。假如有3种以上的质因数,那么前两个数要两种,剩下的最后一个数补位就是一种字典序最小的构造。假如只有1种质因数,那么肯定至少需要6次,前两个数分别要1次和2次,剩下的至少要3次,这也是一种字典序最小的构造。假如有2种质因数,那么总次数<=3的显然是无解的,而总次数>=4的必定有解。首先把两种质因数各1次拿出来,那么剩下的那个2次也不会和前面的重复,只是这样并不是字典序最小(还好题目没要求)。

最简单的思路也是字典序最小的思路就是直接暴力枚举a和b。

下面这个质因数分解的模板貌似还带有筛出最小质因子的功能的。

const int MAXN = 1e5;
int p[MAXN + 5], ptop;
int pn[MAXN + 5];
 
void sieve() {
    int n = MAXN;
    pn[1] = 1;
    for(int i = 2; i <= n; i++) {
        if(!pn[i])
            p[++ptop] = i;
        for(int j = 1; j <= ptop; j++) {
            int t = i * p[j];
            if(t > n)
                break;
            pn[t] = p[j];
            if(i % p[j] == 0)
                break;
        }
    }
}
 
int fac[105][2], ftop;
 
void get_fac(int n) {
    ftop = 0;
    for(int i = 1; i <= ptop; ++i) {
        if(n % p[i] == 0) {
            fac[++ftop][0] = p[i];
            fac[ftop][1] = 0;
            while(n % p[i] == 0) {
                n /= p[i];
                ++fac[ftop][1];
            }
        }
    }
    if(n > 1) {
        fac[++ftop][0] = n;
        fac[ftop][1] = 1;
    }
}
 
void test_case() {
    int n;
    scanf("%d", &n);
    get_fac(n);
    if(ftop >= 3) {
        puts("YES");
        printf("%d %d %d
", fac[1][0], fac[2][0], n / fac[1][0] / fac[2][0]);
        return;
    } else if(ftop == 1) {
        if(fac[1][1] >= 6) {
            puts("YES");
            printf("%d %d %d
", fac[1][0], fac[1][0]*fac[1][0], n / fac[1][0] / fac[1][0] / fac[1][0]);
            return;
        } else {
            puts("NO");
            return;
        }
    } else {
        int sum = fac[1][1] + fac[2][1];
        if(sum <= 3) {
            puts("NO");
            return;
        } else {
            puts("YES");
            printf("%d %d %d
", fac[1][0], fac[2][0], n / fac[1][0] / fac[2][0]);
            return;
        }
    }
}

D - MEX maximizing

题意:有q次操作和一个固定的正整数x,每次操作会往序列中加入一个数字y,你可以在任何时候对序列中的某个y进行任意多次加减x操作(但不能使他们变成负数),求使得整个序列的MEX最大的方案时的MEX的值。

题解:显然模x余数相同的都是同一种等价的东西,可以维护模x值不同结果的cnt。那么MEX操作就相当于询问整个cnt序列中的最小值(这个最小值是一层一层循环叠在下面把MEX垫高),然后再求序列中等于这个最小值的最左边的元素,这个显然可以用线段树来维护(甚至可以支持修改删除)。

但是最简单的操作还是在cnt里面数了之后不断尝试越过MEX即可。

不知道为什么这个操作恰好就是可以用pair的默认操作来维护,但是换成pair之后会编译失败。

struct SegmentTree {
#define ls (o<<1)
#define rs (o<<1|1)
    static const int MAXN = 420000;
    int st[(MAXN << 2) + 5];
    int pos[(MAXN << 2) + 5];
 
    void PushUp(int o) {
        st[o] = min(st[ls], st[rs]);
        if(st[rs] < st[ls])
            pos[o] = pos[rs];
        else
            pos[o] = pos[ls];
    }
 
    void Build(int o, int l, int r) {
        if(l == r) {
            st[o] = 0;
            pos[o] = l;
        } else {
            int m = l + r >> 1;
            Build(ls, l, m);
            Build(rs, m + 1, r);
            PushUp(o);
        }
    }
 
    void Update(int o, int l, int r, int p, int v) {
        if(l == r) {
            st[o] += v;
            return;
        } else {
            int m = l + r >> 1;
            if(p <= m)
                Update(ls, l, m, p, v);
            if(p >= m + 1)
                Update(rs, m + 1, r, p, v);
            PushUp(o);
        }
    }
 
    pii QueryMin(int o, int l, int r, int ql, int qr) {
        if(ql <= l && r <= qr) {
            return {st[o], pos[o]};
        } else {
            int m = l + r >> 1;
            int res = INF;
            int resp = -1;
            if(ql <= m) {
                pii tmp = QueryMin(ls, l, m, ql, qr);
                res = tmp.first;
                resp = tmp.second;
            }
            if(qr >= m + 1) {
                pii tmp = QueryMin(rs, m + 1, r, ql, qr);
                if(tmp.first < res) {
                    res = tmp.first;
                    resp = tmp.second;
                }
            }
            return {res, resp};
        }
    }
#undef ls
#undef rs
} st;
 
void test_case() {
    int q, x;
    scanf("%d%d", &q, &x);
    st.Build(1, 1, x);
    while(q--) {
        int y;
        scanf("%d", &y);
        y = (y % x) + 1;
        st.Update(1, 1, x, y, 1);
        pii tmp = st.QueryMin(1, 1, x, 1, x);
        printf("%d
", tmp.first * x + (tmp.second - 1));
    }
}

E - Obtain a Permutation

题意:给一个n*m的矩阵,要求复原成形如:

1  2  3  4
5  6  7  8
9  10 11 12

的样子,也就是依次填充。

有两种cost都是1的操作,1种是直接修改某位置的数字,另一种是把一列向上轮换。

首先,列与列之间显然是独立的,可以分开考虑,那么就考虑某列怎么复原。最简单的方法就是枚举这一列的开头在哪一行(也就是枚举需要向上),然后每个元素就知道是否对应当前是否处在正确的位置上,每个不在正确位置上的都必须要被换掉。这个复杂度是(单列)n^2的。观察一下容易发现每个元素最多在某次枚举中处在正确位置,这个关系要从等差数列公式推一推。

然后这样做只会会RE,因为访问到了负数。然后修复这个bug只会会WA,想一想其实还会上溢出,因为题目给的数字不一定在n*m以内,这样子确实还是会溢出。

vector<int> a[200005];
int pos[200005];

void test_case() {
    int n, m;
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; ++i) {
        a[i].resize(m + 5);
        for(int j = 1; j <= m; ++j)
            scanf("%d", &a[i][j]);
    }
    ll sumcost = 0;
    int C = n * m;
    for(int j = 1; j <= m; ++j) {
        int mincost = INF;
        for(int i = 0; i < n; ++i)
            pos[i] = 0;
        for(int i = 1; i <= n; ++i) {
            if(a[i][j] > C || (a[i][j] - j) % m != 0)
                continue;
            else {
                int id = i - 1 - ((a[i][j] - j) / m);
                if(id < 0)
                    id += n;
                ++pos[id];
            }
        }
        for(int i = 0; i < n; ++i)
            mincost = min(mincost, n - pos[i] + i);
        sumcost += mincost;
    }
    printf("%lld
", sumcost);
}

F - Three Paths on a Tree

题意:给一棵无权树,选三个点,使得他们之间的简单路径覆盖的边数最多。

题解:在树上上面选三个点,他们的路径必然也是一棵树,所以要求边数最多就是点数最多。其实很显然其中两个点必然是某一条直径的两端。首先,三个点都是叶子,因为若不是叶子,则把这个节点换成某个方向的叶子,一定更好。然后其中两个点必然是某一条直径的两端。否则,把其中两个点换成直径,会更好。所以只需要找出直径,然后找一个到直径距离最远的点。使用多源bfs即可。官方题解提到可以使用dp,仔细想想确实,设dp[i][j]为以1为根时,i的子树内选择j个点,覆盖的边的最大值,因为每次只会和当前子树的根有关所以满足dp的性质。简而言之:dp[u][0]=0,dp[u][1]=max(dp[v][1]+1),dp[u][2]=max(max(dp[v][2]+1),max(dp[v1][1]+dp[v2][1]+1)),dp[u][3]=max(max(dp[v][3]),max(dp[v1][2]+dp[v2][1]+1),max(dp[v1][1]+dp[v2][1]+dp[v3][1]+1))。

使用多源bfs,注意树为一条链的情况,此情况不存在“到直径距离最大且不不为0”的点,故把r3的初始值设为pa[r2],这样就算不更新答案也不会错。

int n;
vector<int> G[200005];
int pa[200005];
int dep[200005];

void dfs(int u, int p) {
    pa[u] = p;
    dep[u] = dep[p] + 1;
    for(auto &v : G[u]) {
        if(v == p)
            continue;
        dfs(v, u);
    }
}

int r1, r2, r3;

queue<int> Q;
int dis[200005];
void bfs() {
    while(!Q.empty())
        Q.pop();
    for(int i = 1; i <= n; ++i)
        dis[i] = -1;
    int ans = 0;
    int x = r2;
    r3 = pa[r2];
    while(x) {
        ++ans;
        Q.push(x);
        dis[x] = 0;
        x = pa[x];
    }
    while(!Q.empty()) {
        int u = Q.front();
        Q.pop();
        for(auto &v : G[u]) {
            if(dis[v] != -1)
                continue;
            dis[v] = dis[u] + 1;
            Q.push(v);
        }
    }
    for(int i = 1; i <= n; ++i) {
        if(dis[i] > dis[r3])
            r3 = i;
    }
    ans += dis[r3];
    printf("%d
%d %d %d
", ans - 1, r1, r2, r3);
}

void test_case() {
    scanf("%d", &n);
    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);
    }
    dfs(1, 0);
    r1 = 0;
    for(int i = 1; i <= n; ++i) {
        if(dep[i] > dep[r1])
            r1 = i;
    }
    dfs(r1, 0);
    r2 = 0;
    for(int i = 1; i <= n; ++i) {
        if(dep[i] > dep[r2])
            r2 = i;
    }
    bfs();
}

反思:分类讨论要仔细。注意构造的几个点会不会发生重合,而且把答案的初始值设为某个“合理”答案而不是非法值“0”、“-1”,虽然在debug阶段可能不容易看出问题,但是假如真的是有问题的话,鲁棒性确实会好一些。

原文地址:https://www.cnblogs.com/KisekiPurin2019/p/12230102.html