Educational Codeforces Round 154 (Rated for Div. 2)【A-E+F(手动翻译)】

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+1<k x+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

题解(翻译+补充):

直接贪心会出问题。如果尽量贪这个数是最大的,然后其他次大的如何分配最小就很难保证。

首先,我们计算每个牌手还需要拿多少张牌,对第 i i i个牌手,记作 r e m a i n i remain_i remaini。我们想要让第 i i i个选手获得的分数最大。我们可以遍历四种花色,让这个选手获得尽可能多的牌。【如果他最后的牌数没拿满到最大的,当我们从其他牌手拿到一张这个花色的牌,并且用另一种花色交换,如果拿到一张牌还是次大,0分不变,如果变成最大了,至少非负,先前是0分,如果最大更大了,+1分,但是如果给的花色刚好是次大的花色牌子,那么又-1分,总之不会变坏。】【就是我们遍历第一个选手所有可能选择的花色】

此时,我们想要找到一种存在的分配方法,使得其他牌手手里剩余的牌的最大值最小。假定是 x x x。需要合理地分配剩余的牌,使得选手 k k k,获得 r e m a i n k remain_k remaink张牌,并且没有任何人拥有超过 x x x张相同花色的牌(除了第 i i i个选手)。

对于 x x x,它具有单调性【如果某种花色的最大可能分配到某人手上的牌数有 n n n,如果我们可以通过分配使得其他选手的某种花色的最大值为 x ( 其中 x < n ) x(其中x<n) x(其中x<n),那么我们一定可以通过交换一张牌,构造出一种 x + 1 x+1 x+1牌数的分配。】

此时,我们可以解决下面这个问题:检查是否存在一种卡片的分配,构造出上述的 x x x的情况,我们可以使用最大流来解决。

  • 创造一个网络,有一个源点,有一个终点,每个除了 i i i之外的牌手的各一个节点,每种花色一个节点。
  • 对于每种花色,添加一条有向边,从源点到这个花色,容量设置为这花色的剩余的牌的数量(注意开始的时候已经给 i i i贪心了某种花色的牌)
  • 对于每个运动员 k ( k ≠ i ) k(k\neq i) k(k=i),添加一条有向边到终点,容量设置为剩余所需的牌的数目。
  • 对于每种花色和每个运动员(除了 i i i),添加一条有向边,从花色到运动员,容量等于这个运动员最多的可以获得的这种花色的牌的数目。
  • 检查当浸润慢所有到达终点的边的容量后的最大流。

但是,简单的建立网络的方法太慢了,更不要说找到其中的最大流。我们需要某种更快的方法。

如果你想用Hall定理,但是不幸的是,Hall定理无法解决 x x x的约束,但是你思考的方向是正确的。

让我们尝试去找到这个网络的最小割。为了做到我们遍历状态码 m a s k mask mask关于四种花色在最小割的集合 S S S里面,然后将剩余的花色的容量加入到这个最小割上,对每个牌手,判断是否有有选定集合里的更好还是最小割其他的集合 T T T更好。关键的事情是,如果我们固定了 m a s k mask mask x x x,这些答案是固定的。把选中的花色的数目记作 s u i t s suits suits,另外第 k k k个牌手已经拥有的牌的花色和数目记作 a l r e a d y H a s m a s k , k alreadyHas_{mask,k} alreadyHasmask,k。全部指向运动员边的第 k k k个牌手的变得容量和,我们需要去割的部分是 s u i t s ⋅ x − a l r e a d y H a s m a s k , k suits ·x-alreadyHas_{mask,k} suitsxalreadyHasmask,k 然后出边是 r e m a i n k remain_k remaink。我们需要添加的割就是两者取小。我们需要高效地计算这些值的和。

这是解法的基本大纲,现在需要实现。

对于每个 m a s k mask mask的每个值 x x x,我们找到 s u i t s ⋅ x − a l r e a d y H a s m a s k , k suits ·x-alreadyHas_{mask,k} suitsxalreadyHasmask,k 的和。为了做到这个,对每个运动员,找到最大的 x x x,我们可以计算那些线性函数的和。和不超过 2 e 6 2e6 2e6

当我们计算对第 i i i个牌手的最小割时(确定 m a s k mask mask x x x),我们可以用已经计算号的函数来找到所有牌手的和,加入到最小割上,之后减去第 i i i个牌手添加的。

注意,当我们通过二分来找到 x x x的左边界的时候,要计算其他其他牌手最大的能拥有的花色的牌数,保证 x x x不会让容量变成负数。

这个模型的预处理的时间复杂度是 O ( 2 K ⋅ ( n ⋅ K + A ) ) O(2^K·(n·K+A)) O(2K(nK+A)),实际的复杂度是 O ( n ⋅ 2 K ⋅ K 2 ⋅ log ⁡ A ) O(n·2^K·K^2·\log A) O(n2KK2logA)其中 A < 2 e 6 A<2e6 A<2e6

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
"educational codeforces round 103 (rated for div. 2)"是一个Codeforces平台上的教育性比赛,专为2级选手设计评级。以下是有关该比赛的回答。 "educational codeforces round 103 (rated for div. 2)"是一场Codeforces平台上的教育性比赛。Codeforces是一个为程序员提供竞赛和评级的在线平台。这场比赛是专为2级选手设计的,这意味着它适合那些在算法和数据结构方面已经积累了一定经验的选手参与。 与其他Codeforces比赛一样,这场比赛将由多个问题组成,选手需要根据给定的问题描述和测试用例,编写程序来解决这些问题。比赛的时限通常有两到三个小时,选手需要在规定的时间内提交他们的解答。他们的程序将在Codeforces的在线评测系统上运行,并根据程序的正确性和效率进行评分。 该比赛被称为"educational",意味着比赛的目的是教育性的,而不是针对专业的竞争性。这种教育性比赛为选手提供了一个学习和提高他们编程技能的机会。即使选手没有在比赛中获得很高的排名,他们也可以从其他选手的解决方案中学习,并通过参与讨论获得更多的知识。 参加"educational codeforces round 103 (rated for div. 2)"对于2级选手来说是很有意义的。他们可以通过解决难度适中的问题来测试和巩固他们的算法和编程技巧。另外,这种比赛对于提高解决问题能力,锻炼思维和提高团队合作能力也是非常有帮助的。 总的来说,"educational codeforces round 103 (rated for div. 2)"是一场为2级选手设计的教育性比赛,旨在提高他们的编程技能和算法能力。参与这样的比赛可以为选手提供学习和进步的机会,同时也促进了编程社区的交流与合作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值