Educational Codeforces Round 154 (Rated for Div. 2)【A-E】【详细题解,F未完待续】

文章目录

  • Educational Codeforces Round 154 (Rated for Div. 2)
    • A. Prime Deletion(模拟)
    • B. Two Binary Strings(思维)
    • C. Queries for the Array(贪心+模拟)
    • D. Sorting By Multiplication(前缀/后缀和)
    • E. Non-Intersecting Subpermutations(dp+前缀和优化)
    • F. Four Suits

Educational Codeforces Round 154 (Rated for Div. 2)

A. Prime Deletion(模拟)

题意:给出一个长度为9,含有1-9字符的串,求一个长度大于等于2的子序列,这个子序列是素数。

思路:取13或者取31

代码:

void solve() {
    string s;
    cin >> s;
    for (auto &x : s) {
        if (x == '1' || x == '3') cout << x;
    }
    cout << '\n';
}

B. Two Binary Strings(思维)

题意:给定两个01串,现有操作,每次可以选择两个相同的字符(同一字符串),并把这两个字符之间的位置都填上这个字符。让你判断这两个字符串通过若干这样的操作能否变成相同的字符串。注意,最左端是0,最右端是1。

思路:实际上,经过操作后我们可以把这些字符串分成若干0块和1块(连续的含有0或1的子串),实际上这种情况再操作一次,取最左侧的1块的第一个1和最右端的1,就能转化成00000111111111的串,任何相同的串都可以转化成这种串,我们判断能否转化成这样的01串就行。枚举所有1的位置,判断能否转化,如果一个是1,一个不是1,那么必定不行,这样没有1的那个串就无法把1串延展的恰好这个位置,要么左侧1,右侧1,必须都是1,同样的,保证0等延伸到这也需要保持相同。

代码:

void solve() {
    string a, b;
    cin >> a >> b;
    int n = a.size();
    for (int i = 1; i < n; i++) {
        if (a[i] == b[i] && a[i] == '1' && a[i - 1] == b[i - 1] && a[i - 1] == '0') {
            cout << "YES\n";
            return;
        }
    }
    cout << "NO\n";
}

C. Queries for the Array(贪心+模拟)

题意:有三种操作,+操作,在数组末尾添加一个数,-操作,移除末尾一个数,0/1操作,检查数组是否是单调不减的,是写1,不是写0。问是否可能出现。

补充:逆序是指较小的数在较大的数后面。

思路:关键是01的位置。以下提供我的思路,首先将字符串划分成一个个含有+±-0/1的子串,因为每个0,1的判断相关联的是部分就是前面的+和-的部分。我维护两个栈,一个栈bad是产生逆序的位置,非空,就会造成这个当前序列不是排序的;一个栈may是记录可能产生逆序的位置。

分两种情况,如果当前子串末尾是1,这种情况比较简单,此时,may栈要清空,因为已经认定当前位置所有数有序了,接下来模拟±操作,用cnt记录当前末端的数是第几个数,当-的时候,检查bad栈顶是否有相同元素,有则弹出,删除了一个已经存在的逆序数。

如果当前子串的末尾是0,这种时候要仔细一些。分两种小清空,首先,如果bad栈是空的,为了让0合法,必须找个位置设置逆序,对于0我们有多个位置选择逆序,这些位置对于0而言都是等价的,但是必须取,但是对于后续的1的影响是不同的,为了后面的1更好的合法,后面的1所更改的范围是有限的,为了更好地覆盖前面逆序的位置,我们选取逆序的位置必须尽量靠后,所以我们取may栈顶的数作为逆序位置。如果may栈是空的,也就是前面有1,或者本次操作内没有可以放置逆序的地方,就是非法的。在本次循环中,如果+的话我们往may栈内增加,-的话要相应减去,确保本次操作可能的逆序的位置入栈。

代码:

void solve() {
    string s;
    cin >> s;
    int n = s.size();
    
    vector<string> op;
    string tmp;
    for (int i = 0; i < n; i++) {
        tmp += s[i];
        if (s[i] == '0' || s[i] == '1') {
            op.push_back(tmp);
            tmp = "";
        }
    }

    stack<int> bad, may;
    int cnt = 0;
    for (auto &x : op) {
        if (x.back() == '1') {
            int n = x.size();
            for (int i = 0; i + 1 < n; i++) {
                if (x[i] == '+') cnt++;
                else {
                    if (!bad.empty() && bad.top() == cnt) {
                        bad.pop();
                    }
                    cnt--;
                }
            }
            while (!may.empty()) may.pop();
            if (!bad.empty()) {
                cout << "NO\n";
                return;
            }
        } else {
            int n = x.size();
            for (int i = 0; i + 1 < n; i++) {
                if (x[i] == '+') {
                    cnt++;
                    if (cnt > 1)
                    may.push(cnt);
                } else {
                    if (!bad.empty() && bad.top() == cnt) {
                        bad.pop();
                    }
                    if (!may.empty() && may.top() == cnt) {
                        may.pop();
                    }
                    cnt--;
                }
            }
            if (bad.empty()) {
                if (may.empty()) {
                    cout << "NO\n";
                    return;
                } else {
                    bad.push(may.top());
                }
            }
        }
    }
    cout << "YES\n";
}

D. Sorting By Multiplication(前缀/后缀和)

题意:给出一个数组,每次操作可以选择一个区间乘上一个数(可正可负),最后使得这个数组有序。问最少要操作几次。

思路:首先,我们考虑全都是正数的情况,如果有一个 a i ⩾ a i + 1 a_i \geqslant a_{i + 1} aiai+1我们至少要操作一次,如果有两个 a i ⩾ a i + 1 a_i\geqslant a_{i+1} aiai+1我们至少要操作两次,同样地,有 k k k个就需要操作 k k k次。可以用数学归纳法证明。此外,关于负数的情况,首先操作后必然是一段连续的负数(或者是空),如果是多段负数,中间的正数必然是非法的,不符合最优的原则。如果有负数的话,我们首先给一个前缀乘上一个负数,这样操作之后,负数前缀的数必定小于正数后缀的数,接下来保证两者内部都是有序的,负数和正数的情况恰好相反,枚举所有的负数前缀,统计使得前缀和后缀有序的操作数,取最小。

另外:为什么可以先乘一个负数,对于选取的前缀,需要让前缀全负并且升序,我们可以按照 a i ⩽ a i + 1 a_i\leqslant a_{i + 1} aiai+1划分乘一段一段,分别乘上合适的负数,最后多出段前缀的非负数,其实等效于我事先乘上一个负数,取全部的选择的负数前缀。

代码:

void solve() {
    int n;
    cin >> n;
    vector<int> a(n);
    for (int i = 0; i < n; i++) {
        cin >> a[i];
    }
    vector<int> l(n), r(n + 1);
    l[0] = 0;
    for (int i = 1; i < n; i++) {
        l[i] = l[i - 1] + (a[i] >= a[i - 1]);
    }
    r[n - 1] = r[n] = 0;
    for (int i = n - 2; i >= 0; i--) {
        r[i] = r[i + 1] + (a[i] >= a[i + 1]);
    }
    int ans = r[0];
    for (int i = 0; i < n; i++) {
        ans = min(ans, l[i] + r[i + 1] + 1);
    }
    cout << ans << '\n';
}

E. Non-Intersecting Subpermutations(dp+前缀和优化)

题意:给你两个数n,k,问长度为n数组,里面是任意的1到k的数,所有出现长度为k连续的两两相同的子数组的数目。

思路:意译了一下官方题解,并且补充了一些说明。

题解:

  • 首先考虑给出一个确定的数组和k,如何计算这个数目呢?可以考虑贪心,尽可能选择靠左的子数组,这样剩余的数越多,更可能选择出更多的子数组。利用哈希表,判断是否出现过当前的数,可以在 O ( n ) O(n) O(n)的时间复杂内实现。我们的目标是选择出后缀k个数不同,我们需要判断当前的数是否出过,出现过需要调整左侧指针,指向上一个相同的数出现的位置后一个数,没有出现过后缀长度会增长1,如果抵达k后缀长度要减到0,答案增加1,因为我们不能重复使用这些数。

  • 设计 d p i , x , c dp_{i,x,c} dpi,x,c是当前长度为 i i i,后缀两两不同的数长度为 x x x,当前有效子数组的数目是 c c c的状态的数组数目。那么此时最后的答案便是 ∑ x = 0 k − 1 ∑ c = 1 n k d p n , x , c ⋅ c \sum^{k-1}_{x=0}\sum^{\frac{n}{k}}_{c=1}{dp_{n,x,c}\cdot c} x=0k1c=1kndpn,x,cc,答案的含义是,对长度为 n n n,后缀有效数字 0 ∼ k − 1 0\sim k-1 0k1的情况,对所有不同价值的数组数对应求和。

  • d p i , x , c dp_{i,x,c} dpi,x,c转移有两种。

    • 新的数不出现在长为 x x x的后缀当中,这种情况比较简单:

      • 其一,转移到 d p i + 1 , x + 1 , c dp_{i+1,x+1,c} dpi+1,x+1,c,这里要求 x + 1 < k x+1x+1<k,没有凑齐新的后缀。
      • 其二,转移到 d p i + 1 , 0 , c + 1 dp_{i+1,0,c+1} dpi+1,0,c+1,这里是刚好 x + 1 = k x+1=k x+1=k,此时记录这个前缀价值加1。
      • 注意点,对于前 i + 1 i+1 i+1的对应种类数,我们需要用前 i i i的对应种类数,乘以当前新的数的选择数也就是 k − x k-x kx,根据乘法原理求出此种转移对后面的贡献。
    • 新的数出现在长为 x x x的后缀当中,这样并不会增加 x x x或者价值数,不会增加有效后缀长度。对于一个长度为 x x x的后缀,这个新出现的数有 1 ∼ x 1\sim x 1x种可能,对于所有出现的数,这个数再原先后缀的位置可能是倒数第 1 ∼ x 1\sim x 1x位置。因此可能转移到 d p i + 1 , 1 ∼ x , c dp_{i+1,1\sim x,c} dpi+1,1x,c,但是直接转移的时间复杂度就是 O ( n k 2 ) O(nk^2) O(nk2),为了减小复杂度,考虑前缀和。注意看。

    • 对于 k − 1 ,转移的范围是 1 2 3 … k − 3 k − 2 k − 1 对于 k − 2 ,转移的范围是 1 2 3 … k − 3 k − 2 对于 k − 3 ,转移的范围是 1 2 3 … k − 3 … 对于 3 ,转移的范围是 1 2 3 对于 2 ,转移的范围是 1 2 对于 1 ,转移的范围是 1 \begin{align*} 对于k-1,转移的范围是 &1\quad 2\quad 3 \dots k-3 \quad k-2 \quad k-1\\ 对于k-2,转移的范围是 &1\quad 2\quad 3 \dots k-3 \quad k-2\\ 对于k-3,转移的范围是 &1\quad 2\quad 3 \dots k-3\\ \dots\\ 对于3,转移的范围是 &1\quad 2\quad 3\\ 对于2,转移的范围是 &1\quad 2\\ 对于1,转移的范围是 &1\\ \end{align*} 对于k1,转移的范围是对于k2,转移的范围是对于k3,转移的范围是对于3,转移的范围是对于2,转移的范围是对于1,转移的范围是123k3k2k1123k3k2123k3123121

    • 我们可以注意到,此类转移对于 d p i + 1 , j , c dp_{i+1,j,c} dpi+1,j,c的贡献。就是对应某列,我们可以用前缀的思想,首先取转移范围最大的,依次累加,可以在 O ( k ) O(k) O(k)的时间复杂度内,得到各列所得到的来自 d p i , x , c dp_{i,x,c} dpi,x,c贡献。

  • 上面的方法已经可以通过了,注意此处的 c c c的上限是 n k \frac{n}{k} kn,所以三维的总空间复杂度是 O ( n 2 ) O(n^2) O(n2)

  • 实际上,第三维度可以省略,每次我们凑出一个完整的后缀的时候,直接计算当前这个后缀对最后的答案的贡献,相当于把原来最后求答案的过程,分散到循环当中,边递推边求答案。 d p i , j dp_{i,j} dpi,j的含义是前 i i i个数,有长度为 j j j的有效后缀,的前缀数组数目。 j = 0 j=0 j=0的时候,就是在当前节点为结尾的长度为 k k k的后缀,的前 i i i个数的排列组合数,乘以后面的其他任意组合的数,就是此处结尾 k k k个数的贡献。注意,后面的数可以随意取,因为即使里面出现了另外的 k k k个的数,并不会重复计算。因为后者一种情况和此处的后缀互斥,不冲突。即使可以同时存在,这两者的贡献都是独立的,并不矛盾。都是合法的答案。

代码:

ll po(ll rad, ll idx) {
	ll res = 1;
	while (idx) {
		if (idx & 1) res = res * rad % mod;
		rad = rad * rad % mod;
		idx >>= 1; 
	}
	return res;
}
int add(int x, int y) {
	return (x + y) % mod;
}
void solve() {
	int n, k;
	cin >> n >> k;
	vector<vector<int>> f(n + 1, vector<int> (k + 1, 0));
	f[0][0] = 1;
	int ans = 0;
	for (int i = 0; i < n; i++) {
		int cur = 0;
		for (int j = k - 1; j >= 1; j--) {
			cur = add(cur, f[i][j]);
			f[i + 1][j] = cur;
		}
		for (int j = k - 1; j >= 0; j--) {
			int nxt = (j + 1) % k;
			f[i + 1][nxt] = add(f[i + 1][nxt], 1ll * f[i][j] * (k - j) % mod);
		}
		ans = add(ans, 1ll * f[i + 1][0] * po(k, n - i - 1) % mod);
	}
	cout << ans << '\n';
}

F. Four Suits

题意:有 n + 1 n+1 n+1个人, n n n个牌手, 1 1 1个发牌人。发牌人有一副牌,包含四种花色(不同花色的牌数目不一定相同)。这副牌的总数可以被 n n n整除。发牌人把全部的牌等分给每个牌手。然后,每个牌手,选择保留手上牌的一种花色,其余牌都弃置。此时比较每个人牌数,拥有最大牌数的人获得最大牌数减去次大牌数的积分,其余人获得零积分。

每个人都想赢,必定会选择手上牌数最多的花色保留。

现在,发牌人已经给了一些牌给选手。第 i i i个选手获得 a i , j a_{i,j} ai,j j j j花色的牌。此时每个牌手手里的牌数不必相等,因为还没有发完牌。此时告诉你发牌人手上对应花色 1 , 2 , 3 , 4 1,2,3,4 1,2,3,4剩余的牌 b 1 , b 2 , b 3 , b 4 b_1,b_2,b_3,b_4 b1,b2,b3,b4。现在请你计算针对每一个牌手的最佳发牌方式,使得其获得最大的分数。

输入:一个数 n n n,一个 n × 4 n\times4 n×4的矩阵, b 1 , b 2 , b 3 , b 4 b_1,b_2,b_3,b_4 b1,b2,b3,b4四个数。

输出: n n n个数,每个牌手最大积分。

范围: 2 ⩽ n ⩽ 5 e 4 2\leqslant n\leqslant 5e4 2n5e4, 0 ⩽ a i , j , b j ⩽ 1 e 6 0\leqslant a_{i,j},b_j\leqslant 1e6 0ai,j,bj1e6

  • 未完待续

你可能感兴趣的:(算法)