250705 学习笔记

前言

知末便知径,径尽方晓终。

... if R is a node on the minimal path from P to Q, knowledge of the latter implies the knowledge of the minimal path from P to R. —— Edsger W. Dijkstra

模拟赛

八点整开始打模拟赛,十一点结束。打了 T1 的 100 分暴力,T2 的 100 分暴力,T3 的 100 分暴力,没有注意到 T6 的 100 分暴力,最后以 361 分收尾。总结一下,没有挂分,算是……

# A B C D E F G
6 361 100 100 100 11 50 - -
- 03:00:00 00:07:36 00:48:49 01:50:28 16:16:16 06:30:19 08:24:49 99:99:99

A. 寻宝游戏

分别计算到两边的步数,取较小者即可。

B. 路灯

分类讨论:1. 两侧的路灯;2. 两路灯中间。还是比较好写的。

C. 收集彩球

首先注意到,可以将颜色看成结点,盒子中上方球的颜色和下方球的颜色之间连边,这样就可以将盒子中的球上下关系转化为一张图。图中有若干连通块,每个块内的颜色球移动不影响块外的球答案(因为块内的颜色和块外不需要也不能进行移动)。所以考虑分别处理每个连通块。下文中,符合要求是指每种颜色的两个球都在同一个盒子中。

可以发现,想要让块内颜色球位置符合要求,需要先将某两种颜色球移动到空盒(\(2\) 步),再将其它颜色球依次配对(每种颜色需要 \(1\) 步),最后空出一个盒子。因此,若一个连通块内有 \(x\) 种颜色且可以符合要求,让它们符合要求需要:

  • \(0\) 步,如果 \(x=1\)(本来就是符合要求的);
  • \(x + 1\) 步,如果 \(x>1\)

现在考虑验证某个块通过移动符合要求可行性的方法。注意到只有 \(1\) 个空盒,所以同一时间最多转移出 \(2\) 个球。因此,考虑设第 \(i\) 种颜色球在上方的盒子数 \(u_i\)。根据题意,\(u_i\) 的取值为 \(0,1\)\(2\)。如果一个连通块内存在 \(2\) 种颜色的 \(u_i=2\),则只用一个空盒无法完成移动。可以基于这个特性对连通块进行验证。

#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
int n, a[N][5];
int fa[N];
int getfa(int x) {return (x == fa[x]) ? x : (fa[x] = getfa(fa[x]));}
void merge(int x, int y) {
	x = getfa(x), y = getfa(y);
	if (x != y) fa[x] = y;	
}
int cnt[N];
vector<int> g[N];
int up[N], pos[N][5];
bool nok(int x) { // 判断连通块是否不符合要求
	int flag = 0;
	for (int i = 0; i < g[x].size(); i++) {
		if (up[g[x][i]] == 2) {
			if (flag) return true;
			flag = g[x][i];
		}
	}
	return false;
}
int main() {
	ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
	cin >> n;
	for (int i = 1; i <= n; i++) fa[i] = i;
	for (int i = 1; i <= n; i++) {
		cin >> a[i][1] >> a[i][2];
		merge(a[i][1], a[i][2]);
		up[a[i][1]]++;
		pos[a[i][1]][1 + (pos[a[i][1]][1] != 0)] = i;
	}
	for (int i = 1; i <= n; i++) {
		int tmp = getfa(i);
		cnt[tmp]++;
		g[tmp].push_back(i);
	}
	int ans = 0;
	for (int i = 1; i <= n; i++) {
		if (cnt[i] == 0 || cnt[i] == 1) continue;
		if (nok(i)) {
			cout << -1 << '\n';
			return 0;
		}
		ans += cnt[i] + 1; 
	}
	cout << ans << '\n';
	return 0;
}

D. 双 v 字形涂色

分类讨论。

  1. 考虑棋盘染色(这里用红、绿),若选择红、绿格各一个,则互不产生影响。
  2. 考虑一个格在另一个格的直角内。此时两个格互不影响。
  3. 若两个 v 交叉,则分别考虑左撞右和右撞左。注意到,第一种情况中的两个部分可以被一条 \(x-y=k\) 的直线分隔开,第二种情况中的两个部分可以被一条 \(x+y=k\) 的直线分隔开。
    将三种情况的答案取最大值得到答案。可以使用动态规划。注意细节处理。
#include <bits/stdc++.h>
using namespace std;
const int N = 3005;
int n, m, a[N][N], ul[N][N], ur[N][N], v[N][N], b[N][N], lv[N][N], rv[N][N], plv[2 * N], prv[2 * N], nlv[2 * N], nrv[2 * N];
int main() {
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        string s;
        cin >> s;
        for (int j = 1; j <= m; j++) a[i][j] = s[j - 1] - '0';
    }
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            if (a[i][j]) {
                ul[i][j] = ul[i - 1][j - 1] + 1;
                ur[i][j] = ur[i - 1][j + 1] + 1;
                v[i][j] = ul[i][j] + ur[i][j] - 1;
            }
            b[i][j] = max({b[i - 1][j - 1], b[i - 1][j], b[i - 1][j + 1], v[i][j]});
        }
    }
    int x = 0, y = 0;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            if ((i + j) & 1) x = max(x, v[i][j]);
            else y = max(y, v[i][j]);
        }
    }
    int ans = x + y; // odd+eve
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            ans = max(ans, v[i][j] + b[i - 1][j]); 
        }
    }
    for (int i = n; i >= 1; i--) {
        for (int j = 1; j <= m; j++) {
            if (a[i][j]) {
                lv[i][j] = max(ul[i][j], lv[i + 1][j - 1] + 1);
                rv[i][j] = max(ur[i][j], rv[i + 1][j + 1] + 1);
                plv[j + n - i + 1] = max(plv[j + n - i + 1], lv[i][j]);
                prv[i + j] = max(prv[i + j], rv[i][j]);
                nlv[j + n - i + 1] = max(nlv[j + n - i + 1], v[i][j]);
                nrv[i + j] = max(nrv[i + j], v[i][j]);
            }
        }
    }
    for (int i = 1; i <= n + m; i++) {
        plv[i] = max(plv[i], plv[i - 1]);
        nrv[i] = max(nrv[i], nrv[i - 1]);
    }
    for (int i = n + m; i >= 1; i--) {
        prv[i] = max(prv[i], prv[i + 1]);
        nlv[i] = max(nlv[i], nlv[i + 1]);
    }
    for (int i = 1; i <= n + m - 1; i++) ans = max({ans, plv[i] + nlv[i + 1], nrv[i] + prv[i + 1]});
    cout << ans << '\n';
    return 0;
}

E. 最大异或

首先考虑 \(3\) 种特殊情况:

  1. \(S\) 中的所有位都是 \(0\)。此时答案显然为 \(0\)
  2. \(S\) 中的所有位都是 \(1\)。根据异或的定义,答案为 \(S\) 是不可能的,因为需要有 \(0\) 才能使所有位都不变。根据贪心的思想,答案要尽可能大需要让高位(靠左侧的位)尽可能大,所以将 \(S\)\(1\) 求异或使得最低位变为 \(0\)
  3. \(S\) 形如 \(00000011111\),即高位均为 \(0\) 低位均为 \(1\)。因为不可能让原本为 \(0\) 的位变为 \(1\),所以此时答案不可能大于 \(S\)。于是将 \(S\)\(0\) 求异或使得答案为 \(S\)

现在考虑一般情况。上面的特殊情况启示了使用贪心法解决这道题,即尽可能使高位为 \(1\)。由此可以看出 \(s_1\)\(s_2\) 必然有其一是 \(S\)。为了方便讨论,不妨设 \(s_1=S\)

注意到,刚才的贪心思路等价于,使从左到右的第一个 \(0\) 尽可能靠右。因此要尽可能填补 \(s_1\) 中的 \(0\)

设去掉前导 \(0\) 后,第一次出现的连续 \(1\) 个数为 \(x\),第一次出现的连续 \(0\) 个数为 \(y\)。显然这 \(y\)\(0\) 只能用 \(x\)\(1\) 中的一些填补。如果 \(x \le y\),则用 \(x\)\(1\) 填补 \(y\)\(0\) 中所在位更高的 \(x\) 个即可;反之,则应将多余的 \(y-x\)\(1\) 去掉,防止将后面的 \(1\) 变为 \(0\)

综上,需要找到从左到右第一个连续的 \(\min(x,y)\)\(1\) 作为 \(s_2\) 的起始部分。接下来即可直接得到完整的 \(s_2\)。这个方法的时间复杂度为 \(\mathcal{O}(n)\)

#include <bits/stdc++.h>
using namespace std;
string s;
char t[10000007];
int n;
int main() {
	ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
	int _;
	cin >> _;
	while (_--) {
		cin >> n >> s;
		s = "#" + s;
		bool all0 = true, all1 = true;
		for (int i = 1; i <= n; i++) {
			if (s[i] == '0') all1 = false;
			if (s[i] == '1') all0 = false; 
		}
		if (all0) {
			cout << "0\n";
			continue;
		} else if (all1) {
			for (int i = 1; i < n; i++) cout << '1';
			cout << "0\n";
			continue;
		}
		int p = 1;
		for (int i = 1; i <= n; i++) if (s[i] == '1') {p = i; break;}
		int q = -1;
		for (int i = p + 1; i <= n; i++) if (s[i] == '0') {q = i; break;}
		if (q == -1) {
			for (int i = p; i <= n; i++) cout << '1';
			cout << '\n';
			continue;
		}
		int r;
		for (r = q; r <= n; r++) {
			if (s[r] == '1') break;
		}
		int x = q - p, y = r - q;
		int cnt = min(x, y);
		int la = p, ra = n, lb = q - cnt, rb = n - cnt;
    	for (int i = ra, j = rb, k = ra - la + 1; i >= la && j >= lb && k >= 1; i--, j--, k--) t[k] = (s[i] != s[j]) ? '1' : '0';
    	for (int i = ra - (rb - lb + 1), k = ra - la + 1 - (rb - lb + 1); i >= la && k >= 1; i--, k--) t[k] = s[i];
		for (int i = 1; i <= n - p + 1; i++) cout << t[i];
		cout << '\n';
	}
	return 0;
} 

F. 拔树游戏

注意到根节点的权值只和它的儿子有关,所以可以想到维护一个小根堆,存储根节点儿子的所有权值。但这样做更新答案时间复杂度过高。

不难发现,可以将选出的子节点的所有儿子都直接加入到堆里。这个操作和原来的方法是等价的。

证明
  • 一步要选的结点在选出子节点的儿子里,则这样操作显然是正确的。
  • 一步要选的结点在根节点的儿子里,则新加的这些节点不会影响选值。所以这样操作仍然是正确的。

综上,这样操作是正确的。

#include <bits/stdc++.h>
using namespace std;
const int N = 3e5 + 5;
struct node {
	int x, id; 
};
const bool operator < (const node &x, const node &y) {return x.x > y.x;}
int n, a[N];
vector<int> g[N];
priority_queue<node> q;
int main() {
	ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
	cin >> n;
	for (int i = 2; i <= n; i++) {
		int fa;
		cin >> fa;
		g[fa].push_back(i);
	}
	for (int i = 1; i <= n; i++) cin >> a[i];
	q.push({a[1], 1});
	for (int t = 1; t <= n; t++) {
		cout << q.top().x << '\n';
		int u = q.top().id;
		q.pop();
		for (int i = 0; i < g[u].size(); i++) {
			int v = g[u][i];
			q.push({a[v], v});
		}
	}
	return 0;
}

回顾

这场模拟赛提示了一些平常学习注意不到的问题,如:

  • 时间分配不合理
  • 错误判断需要使用的知识点
  • 对题目难度和用时估计不准

希望下次比赛注意。

posted @ 2025-07-06 16:48  cwkapn  阅读(18)  评论(0)    收藏  举报