Educational Codeforces Round 82 (Rated for Div. 2)

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

A - Erasing Zeroes

水题

B - National Project

题意:修一条长度为n的路,要求至少一半是good的。注意在每[1,g]天可以修good的路,然后每[g+1,g+b]天可以修bad的路,依次交替。问按要求修完整条路需要的最短时间。

题解:看了一下数据量可以设计一个很显然的二分算法,二分枚举最短时间x,假如一开始设置了二分的下界是n,那么假如不管good和bad总是可以把路修完的,这时就只需要看这x天里面是不是有至少m=(n+1)/2个good即可。不太清楚O(1)是怎么做的,根本不差这点复杂度,大不了确定一下二分的上下界就可以了。

一个更好的下界,应该是max(n,(m/g-1)(g+b)),更好的上界应该是(n/g+1)(g+b),不过这样也就多缩减了一点点,没啥意义。

C - Perfect Keyboard

题意:构造一个26个字母各出现1次的序列,使得题目给的字符串中,相邻两个字符在序列中也是相邻的。

题解:首先假如有某个字符有多于2种邻居,就直接NO,其次假如成环也是直接NO,否则一定会恰好有两个只有1个邻居的点。相当于就是构造一条欧拉路。简洁的写法是直接用set作为“邻接链表”,然后判断是否是欧拉路,是则直接(从度为1的某个点开始)dfs输出,然后把没有被dfs经过的点再补一下。

char s[205];
set<int> G[128];
 
bool vis[128];
 
void dfs(int u, int p) {
    if(vis[u] == 1)
        return;
    putchar(u);
    vis[u] = 1;
    for(auto &v : G[u]) {
        if(v == p)
            continue;
        dfs(v, u);
    }
}
 
void test_case() {
    scanf("%s", s + 1);
    int n = strlen(s + 1);
    if(n == 1) {
        puts("YES");
        puts("abcdefghijklmnopqrstuvwxyz");
        return;
    }
    for(int c = 'a'; c <= 'z'; ++c) {
        G[c].clear();
        vis[c] = 0;
    }
    for(int i = 2; i <= n; ++i) {
        G[s[i]].insert(s[i - 1]);
        G[s[i - 1]].insert(s[i]);
    }
    int cnt1 = 0, head = -1;
    for(int c = 'a'; c <= 'z'; ++c) {
        if(G[c].size() >= 3) {
            puts("NO");
            return;
        }
        if(G[c].size() == 1) {
            ++cnt1;
            head = c;
        }
    }
    if(cnt1 != 2) {
        puts("NO");
        return;
    }
    puts("YES");
    dfs(head, -1);
    for(int c = 'a'; c <= 'z'; ++c)
        dfs(c, -1);
    putchar('
');
    return;
}

当然比赛时想的就比较蠢了,直接往字母的两边放,这样导致很多条件判断。

char s[200005];
char t[105];
char l[256], r[256];
bool vis[256][256];
 
void test_case() {
    scanf("%s", s + 1);
    //puts(s + 1);
    int n = strlen(s + 1);
    if(n <= 3) {
        puts("YES");
        if(s[1] == s[3])
            n = 2;
        if(n == 1) {
            puts("xzytabcdefghijklmnopqrsuvw");
            return;
        }
        if(n == 2) {
            putchar(s[1]);
            putchar(s[2]);
            for(int i = 'a'; i <= 'z'; ++i) {
                if(i == s[1] || i == s[2])
                    continue;
                putchar(i);
            }
            putchar('
');
            return;
        }
        assert(n == 3);
        putchar(s[1]);
        putchar(s[2]);
        putchar(s[3]);
        for(int i = 'a'; i <= 'z'; ++i) {
            if(i == s[1] || i == s[2] || i == s[3])
                continue;
            putchar(i);
        }
        putchar('
');
        return;
    }
    assert(n > 3);
    for(int i = 'a'; i <= 'z'; ++i) {
        l[i] = -1;
        r[i] = -1;
        for(int j = 'a'; j <= 'z'; ++j)
            vis[i][j] = 0;
    }
    for(int i = 2; i <= n; ++i) {
        vis[s[i]][s[i - 1]] = 1;
        vis[s[i - 1]][s[i]] = 1;
    }
    int cnt1 = 0, lmost = -1, rmost = -1;
    for(int i = 'a'; i <= 'z'; ++i) {
        int cnt = 0;
        for(int j = 'a'; j <= 'z'; ++j)
            cnt += vis[i][j];
        if(cnt > 2) {
            puts("NO");
            return;
        }
        if(cnt == 1) {
            ++cnt1;
            if(lmost == -1)
                lmost = i;
            else
                rmost = i;
        }
    }
    if(cnt1 != 2) {
        puts("NO");
        return;
    }
 
    for(int i = 2; i <= n; ++i) {
        if(l[s[i - 1]] == s[i] || r[s[i - 1]] == s[i])
            continue;
        if(r[s[i - 1]] == -1 && l[s[i]] == -1) {
            r[s[i - 1]] = s[i];
            l[s[i]] = s[i - 1];
            continue;
        }
        if(l[s[i - 1]] == -1 && r[s[i]] == -1) {
            l[s[i - 1]] = s[i];
            r[s[i]] = s[i - 1];
            continue;
        }
        puts("NO");
        return;
    }
 
    if(r[lmost] == -1)
        swap(lmost, rmost);
 
    puts("YES");
    memset(t, 0, sizeof(t));
    int top = 0;
    int cur = lmost;
    while(cur != -1) {
        t[top++] = cur;
        cur = r[cur];
    }
    printf("%s", t);
    for(int i = 'a'; i <= 'z'; ++i) {
        int out = 1;
        for(int j = 0; j < top; ++j) {
            if(t[j] == i) {
                out = 0;
                break;
            }
        }
        if(out)
            putchar(i);
    }
    putchar('
');
    return;
}

感觉应该会有人挂n=1的case。

*D - Fill The Bag

题意:有一个容量为x<=1e18的Bag,然后有n个大小为2的非负幂的物品,每次操作可以把一个物品分成等大的两半,求最少的操作把Bag填满。

题解:直接贪心,显然是从x的低位开始构造,先把更低位的物品全部加起来,那么这个范围内的数字都是可以表示,需要的话就直接扣除,毕竟“合并两个等大的物品”是没有花费的。然后假如某位没办法构造的话,就要一直往高位找到第一个有值的,然后一直分两半下来。注意其实这里复杂度应该是一个log的,不过反正两个log也没什么影响。

int cnt[40];
 
void test_case() {
    ll x;
    int n;
    scanf("%lld%d", &x, &n);
    for(int i = 0; i < 32; ++i)
        cnt[i] = 0;
    ll sum = 0;
    for(int i = 1, ai; i <= n; ++i) {
        scanf("%d", &ai);
        sum += ai;
        int k = 0;
        while(ai > 1) {
            ++k;
            ai >>= 1;
        }
        ++cnt[k];
    }
    if(sum < x) {
        puts("-1");
        return;
    }
    int ans = 0;
    sum = 0;
    for(int k = 0; k < 32; ++k) {
        sum += 1ll * cnt[k] * (1ll << k);
        cnt[k] = 0;
        if((x >> k) & 1) {
            if(sum >= (1ll << k)) {
                sum -= (1ll << k);
                continue;
            }
            while(cnt[k] == 0) {
                for(int k1 = k + 1; k1 < 32; ++k1) {
                    if(cnt[k1]) {
                        ++ans;
                        --cnt[k1];
                        cnt[k1 - 1] += 2;
                        break;
                    }
                }
            }
            sum += 1ll * cnt[k] * (1ll << k);
            cnt[k] = 0;
            assert(sum >= (1ll << k));
            sum -= (1ll << k);
            continue;
        }
    }
    printf("%d
", ans);
}

注意:
1、左移运算符也可能会导致int溢出,因为x是ll范围的。
2、提交前消除所有的编译器的warning,scanf的lld的warning其实也可以避免,自己写一个返回ll的快读,也不会浪费多少资源。

ll read() {
    ll x = 0, f = 1;
    char c = getchar();
    while(!isdigit(c)) {
        if(c == '-')
            f = -1;
        c = getchar();
    }
    while(isdigit(c)) {
        x = x * 10 + (c - '0');
        c = getchar();
    }
    return x * f;
}

*E - Erase Subsequences

题意:从一个长度为 (n(1leq n leq 400)) 的字符串 (s) 中,进行至多两次操作,问是否能构造出长度为 (m(1leq mleq 400)) 的字符串 (t) 。每次操作,从字符串 (s) 中取出一个子序列,接在当前字符串 (p) 的后面,初始时, (p) 为空串。同一个字符不能被取出2次。

题解:一开始搞了个贪心,结果连第一个样例都过不了,其实看复杂度就知道过不了了。看一下性质(无后效性)其实很容易想到一种 (dp) 的算法,但是只想到一个 (O(n^4)) 的dp,先枚举字符串 (t) 的前半部分和后半部分的分界线,然后用一个 (dp[i][j][k]) 表示字符串 (s) 的前 (i) 个字符是否可以匹配前半的字符串 (t) 的前 (j) 个字符以及字符串 (t) 的后半的前 (k) 个字符。这个算法不高效的原因很可能是因为lyd说的“没有把状态压缩到极致”,很明显每个 (dp) 只是布尔值的话可以用一些什么bitset优化。别人的正解其实多压缩了一维,也改变了实际上设计的状态,用 (dp[i][j]) 表示字符串 (s) 的前 (i) 个字符匹配字符串 (t) 的前半的前 (j) 个字符时,最多匹配字符串 (t) 的后半的前多少个字符。进行这样的状态压缩,实际上基于一种贪心,因为匹配 (k) 个字符可行,那么匹配少于 (k) 个字符显然都是可行的。

int n, m;
char s[405], t[805];
int dp[405][805];

bool check(int d) {
    for(int i = 0; i <= n; ++i) {
        for(int j = 0; j <= m; ++j)
            dp[i][j] = -INF;
    }
    dp[0][0] = 0;
    for(int i = 1; i <= n; ++i) {
        for(int j = 0; j <= d; ++j) {
            if(dp[i - 1][j] == m - d)
                dp[i][j] = m - d;
            else {
                if(dp[i - 1][j] >= 0)
                    dp[i][j] = dp[i - 1][j] + (s[i] == t[d + 1 + dp[i - 1][j]]);
            }
        }
        for(int j = 1; j <= d; ++j) {
            if(s[i] == t[j])
                dp[i][j] = max(dp[i][j], dp[i - 1][j - 1]);
        }
//        for(int j = 0; j <= d; ++j)
//            printf("dp[%d][%d]=%d
", i, j, dp[i][j]);
    }
//    if(dp[n][d] == m - d)
//        printf("d=%d
", d);
    return (dp[n][d] == m - d);
}

void test_case() {
    scanf("%s%s", s + 1, t + 1);
    n = strlen(s + 1), m = strlen(t + 1);
    for(int i = 1, j = 1; i <= n; ++i) {
        if(s[i] == t[j]) {
            ++j;
            if(j > m) {
                puts("YES");
                return;
            }
        }
    }
    for(int d = 1; d < m; ++d) {
        if(check(d)) {
            puts("YES");
            return;
        }
    }
    puts("NO");
    return;
}

收获:
1、dp时不合法状态最好设为正负无穷这种越界值。
2、在进行下标访问的时候要先判断合法性。
3、对于设计出来的状态是布尔值的dp,假如总是有一半是0另一半是1。这种可以用一个整数来压缩。

*G - Sum of Prefix Sums

学长提了一下点分治,想一想确实有可能。

题意:对于一个数字序列来说,定义一个 (f=sumlimits_{i=1}^{n}sumlimits_{j=1}^{i}a_j) 。对于一棵点带权的树,每个path对应唯一的数字序列,求这个最大值。

想法:既然先天知道是点分治,那么定下一个根r之后,把path分为4类:

1、从r出发,到某个子树结束
2、从子树出发,到r结束
3、从某个子树出发,到另一个子树结束
4、从某个子树出发,到同一个子树结束

其中第4类可以递归给各个子树分治去统计,只需要考虑第1类和第2类,因为第3类可以断成两段,一段是第1类,另一段是第2类。

那么假如可以统计出某子树中根到每个叶子的权值和深度的乘积的和的最大值,貌似可以转移,但是这样要保留所有根到叶子的信息才可以。还是等题解吧。

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