【题解】Educational Codeforces Round 144(CF1796)

发布时间 2023-09-07 20:59:42作者: linyihdfj

被 C 卡了。
最后被 E 的各种分讨劝退,但是同时也学习到了一种换根 dp 的简单写法。
评价:It's educational for me.

A.Typical Interview Problem

题目描述:

有一个包含 F 和 B 的字符串,最开始是空的。我们开始从 \(1\) 向后遍历所有整数,按照下面的规则构成字符串:

  • 如果该整数能被 \(3\) 整除,则在该字符串后加入 F
  • 如果该整数能被 \(5\) 整除,则在该字符串后加入 B

特别的,如果该整数能同时被 \(3\)\(5\) 整除,则先加入 F 再加入 B。

现在有 \(t\) \((1\le t\le2046)\) 次询问,每个给出一个长度为 \(k\) \((1\le k \le 10)\) 的字符串 \(s\),请回答该字符串是否是上面字符串的子串(需要连续)。(保证给出的字符串仅包含 F 和 B)

题目分析:

构成的字符串显然是有循环节的,所以直接打表出来几个循环节,然后判断 \(s\) 是否出现就可以了,可以暴力判断。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
string s,t;
bool chk(int pos,string t){
	for(int i=0; i<t.size(); i++){
		if(t[i] != s[pos + i])	return false;
	}
	return true;
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	for(int i=1; i<=300; i++){
		if(i % 3 == 0)	s = s + 'F';
		if(i % 5 == 0)	s = s + 'B';
	}
	int T;scanf("%d",&T);
	while(T--){
		int len;
		cin>>len>>t;
		bool flag = false;
		for(int i=0; i<s.size(); i++){
			if(i + len - 1 < s.size() && chk(i,t))	flag = true;
		}
		if(flag)	printf("Yes\n");
		else	printf("No\n");
	}
	return 0;
}

B.Asterisk-Minor Template

题目描述:

定义一个字符串的模板 \(T\),其中仅包含小写字母和可以代替任意字符串(包括空字符串)的通配符 '*'。一个合法的模板需要满足 '*' 的数量小于等于小写字母的数量。

有一个仅包含小写字母的字符串 \(s\),如果我们可以选择出一个合法的模板 \(T\),可以通过替换所有的 '*' 使其等于 \(s\),则称字符串 \(s\) 与模板 \(T\) 匹配。

现在有 \(t\) $(1 \le t \le 10^4) $ 组询问,每次给出两个仅包含小写字母的字符串 \(a,b\) \((1\le|a|,|b|\le 50)\),请给出一个合法的模板 \(T\) 使其能同时匹配两个字符串,或输出无解 "NO"。

题目分析:

对于一个模板 \(T\),其一定可以表示为 *cc**cd* 的形式,所以可以对于每一个都判断一下就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 100;
char s[N],t[N]; 
bool flag1[N][N];
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int T;scanf("%d",&T);
	while(T--){
		scanf("%s%s",s+1,t+1);
		int n = strlen(s+1);
		int m = strlen(t+1);
		bool flag = false;
		for(int i=1; i<n; i++)	flag1[s[i]-'a'][s[i+1]-'a'] = true;
		for(int i=1; i<m; i++){
			if(flag1[t[i]-'a'][t[i+1]-'a']){
				flag = true;
				printf("Yes\n*%c%c*\n",t[i],t[i+1]);
				break;
			}
		}
		if(s[1] == t[1] && !flag)	printf("Yes\n%c*\n",s[1]),flag = true;
		if(s[n] == t[m] && !flag)	printf("Yes\n*%c\n",t[m]),flag = true;
		if(!flag)	printf("No\n");
		for(int i=1; i<n; i++)	flag1[s[i]-'a'][s[i+1]-'a'] = false;
	}
	return 0;
}

C.Maximum Set

题目描述:

定义一个集合 \(S\) 是合法的,当且仅当集合中任意两个整数 \(x\)\(y\) 满足 \(x\)\(y\) 整除或 \(y\)\(x\) 整除。

\(t\) \((1\le t\le 2\times10^4)\) 次询问,每次给出整数 \(l,r\) \((1\le l\le r \le 10^6)\),每次询问给出两个回答,第一个是在集合 \([l,r]\) 中选出整数组成的合法集合的最大大小,第二个是大小最大的合法集合的个数。

题目分析:

集合里面的数肯定是由最小值每次乘某个数然后扩展得到的。
为了使得集合大小最大,显然只能乘 \(2\) 或者少量的 \(3\),因为如果乘 \(4\) 就显然不如乘两次 \(2\),这样可以让集合大小加 \(1\)
所以可以直接通过 \(l\) 一直乘 \(2\) 进行扩展,这样就可以得到集合最大的大小,然后一个个地将乘 \(2\) 替换为乘 \(3\),计算并计算有多少个最小值符合条件。
这样为什么是合法的,考虑在我们一步步替换的过程中集合的大小不会变,集合大小其实就是质因数分解之后的指数之和,而我们指数最多为 \(O(\log n)\) 所以可以暴力枚举。
可以发现若当前乘的值为 \(p\),那么最小值 \(x\) 满足条件当且仅当:\(x \in [l,r]\)\(xp \in [l,r]\),化简一下就是 \(x \in [l,\lfloor \frac{r}{p} \rfloor]\)
需要注意的一点就是,我们乘 \(2\) 和乘 \(3\) 不同的顺序会造成集合的不同,先乘 \(2\) 和先乘 \(3\) 并不一样,所以贡献要带一个组合数。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MOD = 998244353;
int C[105][105];
int binom(int n,int m){
	if(n < m || n < 0 || m < 0)	return 0;
	return C[n][m];
}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	C[0][0] = 1;
	for(int i=1; i<=100; i++){
		C[i][0] = 1;
		for(int j=1; j<=i; j++){
			C[i][j] = (C[i-1][j] + C[i-1][j-1])%MOD;
		}
	}
	int T;scanf("%lld",&T);
	while(T--){
		int l,r;scanf("%lld%lld",&l,&r);
		int tmp = 1,cnt = 0;
		while(tmp * l <= r)	tmp *= 2,cnt++;
		--cnt;tmp /= 2;
		int ans = 0;
		ans = (ans + r/tmp - l + 1)%MOD;
		for(int i=1; i<=cnt; i++){
			tmp = tmp / 2 * 3;
			if(l * tmp > r)	break;
			ans = (ans + (r/tmp - l + 1) * binom(cnt,i))%MOD;
		}
		printf("%lld %lld\n",cnt+1,ans);
	}
	return 0;
}

D.Maximum Subarray

题目描述:

你得到了一个序列 \(a_1,a_2,\cdots,a_n\),由 \(n\) 个整数组成。并且你还得到了两个整数 \(k,x\)

你需要执行一次操作:选择恰好 \(k\) 个不同的位置加上 \(x\),其余位置减去 \(x\)

比如说:如果 \(a=[2,-1,2,3],k=1,x=2\),然后我们选择第一个位置,操作之后的 \(a=[4,-3,0,1]\)

定义 \(f(a)\)\(a\) 序列的子串的最大可能和。\(a\) 的子串是 \(a\) 的一部分,即序列 \(a_i,a_{i+1},\cdots a_j\) 对于某个 \(1\le i\le j\le n\)。同时,空数组也应该被考虑,它的和为 \(0\)

\(a'\)\(a\) 操作后的序列,输出 \(f(a')\) 的最大可能值。

\(1\le t\le 10^4;\;1\le n,\sum n\le 2\cdot 10^5;\;0\le k\le \min(20,n);\;-10^9\le a_i,x\le 10^9\)

题目分析:

认为 \(x\) 一定为正数,因为若 \(x\) 为负数显然可以 \(-x \to x,n-k \to k\),就能让 \(x\) 变为正数。
若钦定区间 \([l,r]\) 为我们选择的最优区间,则若区间长度小于 \(k\) 则只能加区间长度个 \(x\),否则只能加 \(k\)\(x\) 并且还有一些位置必须加 \(-x\)
所以下面的一个想法就是,因为要考虑每一个子串,所以枚举右端点 \(r\)
这样我们就可以对于不同的区间的长度进行分类讨论不同的贡献。
若区间长度小于 \(k\),注意到 \([l,r]\) 的区间和可以写为 \(s_r - s_{l-1}\),所以只需要预先对每个位置都加 \(x\),这样一个长度小于等于 \(k\) 的区间的权值和就对了。
若区间长度大于等于 \(k\),因为减 \(x\) 的数量与区间长度有关,而加 \(x\) 的数量一定,所以不妨将每个位置都减 \(x\),这样只需要将对应的区间和加 \(2kx\) 就是正确的权值和。
每一个都相当于一个一定长度的最大子段和,就相当于 \(s_r - s_{l-1}\) 中固定了 \(s_r\)\(l\) 在一定范围内的 \(s_{l-1}\) 的最小值。
这个可以使用 ST 表维护,复杂度 \(O(n \log n)\)

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5+5;
const int INF = 1e18+5;
int st1[N][22],st2[N][22];
int pre1[N],pre2[N];
int query1(int l,int r){
	if(l > r)	return INF;
	int len = log2(r-l+1);
	return min(st1[l][len],st1[r-(1<<len)+1][len]);
}
int query2(int l,int r){
	if(l > r)	return INF;
	int len = log2(r-l+1);
	return min(st2[l][len],st2[r-(1<<len)+1][len]);
}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	memset(st1,0x3f,sizeof(st1));
	memset(st2,0x3f,sizeof(st2));
	int T;scanf("%lld",&T);
	while(T--){
		int n,k,x;scanf("%lld%lld%lld",&n,&k,&x);
		if(x < 0)	x = -x,k = n - k;
		st1[0][0] = 0,st2[0][0] = 0;
		for(int i=1; i<=n; i++){
			int a;scanf("%lld",&a);
			pre1[i] = pre1[i-1] - x + a;
			pre2[i] = pre2[i-1] + x + a;
			st1[i][0] = pre1[i];
			st2[i][0] = pre2[i];
		}
		for(int i=1; i<=20; i++){
			for(int j=0; j+(1<<i)-1<=n; j++){
				st1[j][i] = min(st1[j][i-1],st1[j+(1<<(i-1))][i-1]);
				st2[j][i] = min(st2[j][i-1],st2[j+(1<<(i-1))][i-1]); 
			}
		}
		int ans = 0;
		for(int i=1; i<=n; i++){
			if(i >= k){
				int tmp = query1(0,i-k);
				ans = max(ans,pre1[i] - tmp + 2 * x * k);
			}
			int tmp = query2(max(i-k+1,0ll),i);
			ans = max(ans,pre2[i] - tmp);
		}
		printf("%lld\n",ans);
		for(int i=0; i<=20; i++){
			for(int j=0; j+(1<<i)-1<=n; j++){
				st1[j][i] = INF;
				st2[j][i] = INF;
			}
		}
	}
	return 0;
}

E.Colored Subgraphs

题目描述:

Alice有一棵 \(n\) 个节点的树。

他要选择一个节点 \(r\),然后:

  • 设一个数组 \(d\)\(d_i\) 表示 \(r\) 点到 \(i\) 点的距离。
  • 并把所有节点都染个色。

但染色必须要满足两个条件:

  • 对于任意被染了相同颜色的点对 \((x,y)\),从点 \(x\) 到点 \(y\) 的路径上所有点(包括点 \(x\) 和点 \(y\))的颜色都一样。
  • 对于任意被染了相同颜色的点对 \((x,y)\),条件 \(d_x \not= d_y\) 必须都满足。

现在,定义一个染色方案的代价为一种颜色的最小出现次数,即 \(\min(\text{cnt}_i)\)

现在,他让你选择一个最优的节点 \(r\),使得染色方案的最小代价最大。
\(1 \le n \le 2\times 10^5\)

题目分析:

要解决这个问题显然需要换根 \(dp\),所以现在的问题就是给定一个 \(r\) 怎么求解答案。
考虑一个贪心的想法就是每种颜色从叶子向上扩展,当某一个节点有多个儿子,则选择所在的链长度最短的一个儿子的颜色作为当前点的颜色,然后向上扩展这一条链。
这个过程放到 \(dp\) 下理解就是,设 \(f_u\) 表示以 \(u\) 为根的子树按照上述过程操作之后的 \(u\) 所在的链长度。
转移的话显然就是从儿子里选一个最小的转移,没被选择的部分对答案造成贡献,而显然对答案造成贡献的就是所有儿子的次小值,所以只需要记一下次小值。
而换根的话就是考虑根由 \(u \to v\),那么就将 \(v\)\(u\) 的贡献删去,然后加入 \(u\)\(v\) 的贡献,为了维护次小值就必须同时维护第三小值,就比较爆炸。
一个很好写的做法就是,对于每个点 \(u\) 维护 \(f_u\) 表示 \(u\) 所有儿子所在链的长度,这样对 \(u\) 父亲的贡献就是 \(f_u\) 中的最小值,对答案的贡献就是 \(f_u\) 中的次小值,关键是换根的之后只需要在 \(f_u\) 中删去 \(v\) 的贡献然后在 \(f_v\) 中加入 \(u\) 的贡献即可,省去了所有的分类讨论,产生的影响只是复杂度多了一个 \(O(\log n)\),而在一般的换根 \(dp\)\(O(n)\)\(O(n \log n)\) 显然没区别。
要注意的就是根对答案的贡献为 \(f\) 中的最小值。

在代码实现的时候我默认 \(f_u\) 中的所有状态不包含 \(u\) 这个点,所以要注意一下细节。

代码:

点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int INF = 0x3f3f3f3f;
const LL mod = 1e9 + 7;
const int N = 200005;

vector<int> G[N];
multiset<int> f[N]; 
multiset<int> se;   
int ans;

int getlen(int u) {
    return f[u].size() == 0 ? 1 : *f[u].begin() + 1;
}
void add(int u, int val) {
    if (f[u].size() >= 2) se.erase(se.find(*next(f[u].begin())));
    f[u].insert(val);
    if (f[u].size() >= 2) se.insert(*next(f[u].begin()));
}
void del(int u, int val) {
    if (f[u].size() >= 2) se.erase(se.find(*next(f[u].begin())));
    f[u].erase(f[u].find(val));
    if (f[u].size() >= 2) se.insert(*next(f[u].begin()));
}

void dfs1(int u, int fa) {
    for (auto v : G[u]) {
        if (v == fa) continue;
        dfs1(v, u);
        f[u].insert(getlen(v));
    }
    if (f[u].size() >= 2) se.insert(*next(f[u].begin()));
}
void dfs2(int u, int fa) {
    ans = max(ans, min(getlen(u), se.empty() ? INF : *se.begin()));
    for (auto v : G[u]) {
        if (v == fa) continue;
        del(u, getlen(v));
        add(v, getlen(u));
        dfs2(v, u);
        del(v, getlen(u));
        add(u, getlen(v));
    }
}

int main() {
    int _;
    scanf("%d", &_);
    while (_--) {
        int n;
        scanf("%d", &n);
        se.clear();
        for (int i = 1; i <= n; i++) G[i].clear(), f[i].clear();
        for (int i = 1; i < n; i++) {
            int u, v;
            scanf("%d%d", &u, &v);
            G[u].push_back(v);
            G[v].push_back(u);
        }
        ans = 0;
        dfs1(1, 0);
        dfs2(1, 0);
        printf("%d\n", ans);
    }
    return 0;
}