Codeforces Round #798 (Div. 2)

这篇博客介绍了四道算法题的解决方案,涉及字符串操作、全排列、二叉树传播和图联通性问题。题目包括最小字典序构建、错排排列、树形DP求解未感染节点以及曼哈顿距离最大黑格子的最短距离。文章通过贪心和动态规划策略给出了高效的算法实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

link
期末结束了,开始训练。不知道是太久没做题了还是太弱了,感觉连ABC都写了挺久的,所以就都写一份题解吧。

A

题意

给出两个字符串以及一个初始为空的答案串,每次可以从两个字符串中任选一个字符移动到答案串的末尾,直到某一串为空,问答案串的最小字典序。其中,同一个串不能连续取超过 k k k 次。(保证两个串没有相同字母)

思路

因为和顺序无关,所以首先将2个串内部分别按字典序从小到大排序,然后贪心地取小的即可,并维护取到哪个串以及连续取了该串的多少个,等于 k k k 时就强制换串。因为保证了没有相同字母,所以不需要考虑相同的情况。

代码

int n, m, k;
void solve() {
    cin >> n >> m >> k;
    string s1, s2, s3;
    cin >> s1 >> s2;
    sort(s1.begin(), s1.end());
    sort(s2.begin(), s2.end());
    s3.clear();
    int cnt = 0;
    int last =-1;
    while(s1.size() && s2.size()) {
    	if(cnt < k) {
    		cnt++;
    		if(*s1.begin() < *s2.begin()) {
    			s3.pb(*s1.begin());
    			s1.erase(s1.begin());
    			if(last != 1) {
    				last = 1;
    				cnt = 1;
    			}
    		}
    		else {
    			s3.pb(*s2.begin());
    			s2.erase(s2.begin());
    			if(last != 2) {
    				last = 2;
    				cnt = 1;
    			}
    		}
    	}
    	else {
    		if(last == 1) {
    			last = 2;
    			cnt = 1;
    			s3.pb(*s2.begin());
    			s2.erase(s2.begin());
    		}
    		else {
    			last = cnt = 1;
    			s3.pb(*s1.begin());
    			s1.erase(s1.begin());
    		}
    	}
    }
    cout << s3 << endl;
}

B

题意

给出一个全排列,求在满足错排条件下的最小字典序排列。

思路

可以直接贪心,用一个 s e t set set 维护当前还未被取的数,能取最小的就取最小的,不能就取集合中第二小的,若最后一个数不满足错排只需要交换最后两个数即可。

代码

int a[maxn], n;
void solve() {
    cin >> n;
    set<int> s;
    for(int i = 1; i <= n; i++) {
    	cin >> a[i];
    	s.insert(i);
    }
    if(n == 1) {
    	cout << -1 << endl;
    	return ;
    }
    vector<int> ans;
    ans.pb(0);
    for(int i = 1;i <= n; i++) {
    	if(a[i] != *s.begin() || i == n) {
    		ans.pb(*s.begin());
    		s.erase(s.begin());
    	}
    	else {
    		ans.pb(*s.begin()+1);
    		s.erase(*(s.begin())+1);
    	}
    }
    if(ans[n] == a[n]) {
    	swap(ans[n], ans[n-1]);
    }
    for(int i = 1; i <= n; i++) {
    	cout << ans[i] <<' ';
    }
    cout << endl;
}

C

题意

给出一棵根为1的二叉树,有一个病毒从根开始传播,每轮会传播到其中一个儿子,同时你每轮可以选择一个节点砍掉,如果这样做则以该结点为根的子树都不会被感染。问最多能有多少个节点既没有被感染也没有被砍掉?

思路

这道题用两个标记来标记有几个儿子还算挺妙的,学到了
树形dp,用 cnt[] 和 dp[] 数组分别维护以 i i i 为根的子数的节点数量以及最多有多少个节点满足条件。
因为是二叉树,所以如果一个节点只有一个儿子,那么我们可以直接把它的儿子砍掉,dp[i] = cnt[son] - 1 。两个儿子的话 dp[i] = max(dp[son1] + cnt[son2], dp[son2] + cnt[son1]) - 1

D

1900

题意

给你一个由黑白两种颜色构成的 n × m ( n , m ≤ 1000 ) n\times m(n,m\leq 1000) n×m(n,m1000) 的矩阵,请你找到某个坐标 ( i , j ) (i,j) (i,j) ,使得离这个坐标曼哈顿距离最大的黑格子距离最小。

思路

想了一会才发现,无论图上有多少个黑格子,与任意一个点曼哈顿距离最大的一定是“左上,左下,右上,右下”(最多)四个顶点之一。另一方面,如果我们知道了这四个点坐标,那么对任意点都可以 O ( 1 ) O(1) O(1) 得出答案。所以考虑如何求这四个点。

  1. 右下和左上:显然分别是 max ⁡ ( i + j ) 和 min ⁡ ( i + j ) \max(i+j) 和\min(i+j) max(i+j)min(i+j)
  2. 右上和左下:画个图发现右上是 max ⁡ ( j − i ) \max(j-i) max(ji) ,左下是 max ⁡ ( i − j ) \max(i-j) max(ij)

当然最后求出来的“右上”不一定对每个点都处在右上方,但并不影响答案,因为此时“右上”一定不会构成最大值。

代码

int n, m;
char a[1010][1010];
pii ru, rd, lu, ld;
void judge(int i, int j) {
	if(i + j < lu.x + lu.y) {
		lu.x = i, lu.y = j;
	}
	if(i + j > rd.x + rd.y) {
		rd.x = i, rd.y = j;
	}
	if(j - i > ru.y - ru.x) {
		ru.x = i, ru.y = j;
	}
	if(i - j > ld.x - ld.y) {
		ld.x = i, ld.y = j;
	}
}
int cal(int i, int j) {
	int ans = abs(rd.x-i)+abs(rd.y-j);
	chmax(ans, abs(lu.x-i)+abs(lu.y-j));
	chmax(ans, abs(ru.x-i)+abs(ru.y-j));
	chmax(ans, abs(ld.x-i)+abs(ld.y-j));
	return ans;
}
void solve() {
    cin >> n >> m;
    rd = mp(0, 0);
    lu = mp(INF, INF);
    ru = mp(INF, 0);
    ld = mp(0, INF);
    for(int i = 1; i <= n; i++) {
    	for(int j = 1; j <= m; j++) {
    		cin >> a[i][j];
    		if(a[i][j] == 'B') {
    			judge(i, j);
    		}
    	}
    }
    int ans = INF;
    int xx = 0, yy = 0;
    for(int i = 1; i <= n; i++) {
    	for(int j = 1; j <= m; j++) {
    		if(cal(i, j) < ans) {
    			ans = cal(i, j);
    			xx = i, yy = j;
    		}
    	}
    }
    cout << xx << ' ' << yy << endl;
}

E

2500

题意

给出一个序列 n ( n ≤ 2000 ) n(n\leq 2000) n(n2000),我们定义 i i i j j j 之间存在一条边当且仅当 a i & a j ≠ 0 a_i \&a_j \neq 0 ai&aj=0, 可以进行以下两种操作任意次:

  1. 选一个 i i i ,使 a i + 1 a_i+1 ai+1
  2. 选一个 i i i ,使 a i − 1 a_i-1 ai1 ,其中 a i > 0 a_i > 0 ai>0

问至少多少操作次使得序列构成的图是联通的?

思路

首先如果这个序列中有0,我们必须把它们先变成1。
此时的序列有如下结论:至多操作2次满足联通
方法:
l b ( x ) lb(x) lb(x) x x x 的 lowbit 值, m l b mlb mlb max ⁡ ( l b ( x ) ) , x ∈ 序 列 \max(lb(x)),x\in 序列 max(lb(x)),x ,这里假设 x x x 的 lowbit 是最大的,那么我们只需要将 x x x 减1,则所有 lowbit 小于 l b ( x ) lb(x) lb(x) 的数都满足和 x x x 连通,剩下的可能不与 x x x 连通的数 lowbit 也一定等于 l b ( x ) lb(x) lb(x), 然后我们只需要找其中一个 +1 就可以了。

上面这两种都是先考虑减,但还有一种特殊情况,比如 1010 , 0110 , 0001 1010,0110,0001 1010,0110,0001,就需要选一个数加1,最少一次就可以得到。

总之具体做的时候枚举每个数++,- -,++,时间复杂度 O ( 30 n 2 ) O(30n^2) O(30n2)

代码

int n;
int a[maxn];
int fa[maxn], sz[maxn];
int find(int x) {
	if(fa[x] == x) return x;
	return fa[x] = find(fa[x]);
}
void merge(int x, int y) {
	int fx = find(x);
	int fy = find(y);
	int tmp = sz[fx] + sz[fy];
	if(fx != fy) {
		fa[fy] = fx;
		sz[fx] = tmp;
	}
}
int lb(int x) {
	return x & (-x);
}
void init() {
	for(int i = 0; i <= 30; i++) fa[i] = i, sz[i] = 1;
}
bool check() {
	init();
	int k = 0;
	for(int i = 1; i <= n; i++) {
		k |= a[i];
	}
	for(int i = 1; i <= n; i++) {
		int la = -1;
		for(int j = 0; j <= 30; j++) {
			if((a[i] >> j) & 1) {
				if(la != -1) merge(la, j);
				la = j;
			}
		}
	}
	set<int> s;
	for(int i = 0; i <= 30; i++) {
		if((k >> i) & 1) s.insert(find(i));
	}
	return s.size() == 1;
}
int ans = 0;
void print() {
	cout << ans << endl;
	for(int i = 1; i <= n; i++) {
		cout << a[i] << ' ';
	}
	cout << endl;
	return;
}
void solve() {
    cin >> n;
    ans = 0;
    for(int i = 1; i <= n;i++) {
    	cin >> a[i];
    	if(!a[i]) a[i]++, ans++;
    }
    if(check()) {
    	print();
    	return;
    }
    for(int i = 1; i <= n; i++) {
    	a[i]++;
    	ans++;
    	if(check()) {
    		print();
    		return;
    	}
    	ans--;
    	a[i]--;
    }
    int id = 0, mlb = 0;
    for(int i = 1; i <= n; i++) {
    	if(lb(a[i]) > mlb) {
    		id = i;
    		mlb = lb(a[i]);
    	}
    }
    vector<int> v;
    for(int i = 1; i <= n; i++) {
    	if(lb(a[i]) == mlb) {
    		a[i]--;
    		ans++;
    		if(check()) {
    			print();
    			return;
    		}
    		ans--;
    		a[i]++;
    	}
    }
    a[id]--;
    ans++;
    if(check()) {
    	print();
    	return;
    }
    for(int i = 1; i <= n; i++) {
    	if(i == id) continue;
    	a[i]++;
    	ans++;
    	if(check()) {
    		print();
    		return;
    	}
    	ans--;
    	a[i]--;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值