【题解】Educational Codeforces Round 144(CF1796)

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

A.Typical Interview Problem

题目描述:

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

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

特别的,如果该整数能同时被 35 整除,则先加入 F 再加入 B。

现在有 t (1t2046) 次询问,每个给出一个长度为 k (1k10) 的字符串 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 (1t104) 组询问,每次给出两个仅包含小写字母的字符串 a,b (1|a|,|b|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 是合法的,当且仅当集合中任意两个整数 xy 满足 xy 整除或 yx 整除。

t (1t2×104) 次询问,每次给出整数 l,r (1lr106),每次询问给出两个回答,第一个是在集合 [l,r] 中选出整数组成的合法集合的最大大小,第二个是大小最大的合法集合的个数。

题目分析:

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

题目描述:

你得到了一个序列 a1,a2,,an,由 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 的一部分,即序列 ai,ai+1,aj 对于某个 1ijn。同时,空数组也应该被考虑,它的和为 0

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

1t104;1n,n2105;0kmin(20,n);109ai,x109

题目分析:

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

代码:

点击查看代码
#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,然后:

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

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

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

现在,定义一个染色方案的代价为一种颜色的最小出现次数,即 min(cnti)

现在,他让你选择一个最优的节点 r,使得染色方案的最小代价最大。
1n2×105

题目分析:

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

在代码实现的时候我默认 fu 中的所有状态不包含 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;
}
posted @   linyihdfj  阅读(31)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示