2021ICPC沈阳区域赛BEFIJM

2021ICPC沈阳区域赛BEFIJM

E. Edward Gaming, the Champion

题意

给定长度不超过 2 e 5 2\mathrm{e}5 2e5的文本串,统计其中"edgnb"出现的次数.

代码 -> 2021ICPC沈阳-E(KMP)

const int MAXN = 2e5 + 5;
int n = 5, m;  // 模式串长度、文本串长度
char p[MAXN] = "0edgnb", s[MAXN];  // 模式串、文本串
int nxt[MAXN];  // KMP的next数组

void init() {  // 预处理出模式串的next数组
	for (int i = 2, j = 0; i <= n; i++) {
		while (j && p[i] != p[j + 1]) j = nxt[j];
		if (p[i] == p[j + 1]) j++;
		nxt[i] = j;
	}
}

int main() {
	init();

	cin >> s + 1; m = strlen(s + 1);
	int ans = 0;
	for (int i = 1, j = 0; i <= m; i++) {
		while (j && s[i] != p[j + 1]) j = nxt[j];
		if (s[i] == p[j + 1]) j++;
		if (j == n) {
			ans++;
			j = nxt[j];
		}
	}
	cout << ans;
}


F. Encoded Strings I

题意

给定一个长度为 n    ( 1 ≤ n ≤ 1000 ) n\ \ (1\leq n\leq 1000) n  (1n1000)、只包含前 20 20 20个小写英文字母的字符串 s s s,将其所有非空前缀按如下规则解码,输出解码后字典序最大的字符串.解码规则:设 s s s中的字符 c c c s s s中最后一次出现的位置之后(不包括该位置)有 x x x个不同字符,将 c c c替换为第 ( x + 1 ) (x+1) (x+1)个英文字母.

样例解释

注意到 s s s的最后一个字符必是该字母最后一次出现的位置,且其后无其他字母,故 s s s中所有该字母必会被替换为a.

显然 s s s中的所有相同字母会被替换成同一的字母.

字符串下标从 0 0 0开始.

[样例I] s = s= s=“abc”

​ ①a字母最后一次出现的位置是 s [ 0 ] s[0] s[0],其后有b、c两个不同字母,故将其替换为第 ( 2 + 1 ) (2+1) (2+1)个字母,即c.

​ ②b字母最后一次出现的位置是 s [ 1 ] s[1] s[1],其后有c一个不同字母,故将其替换为第 ( 1 + 1 ) (1+1) (1+1)个字母,即b.

​ ③c字母是最后一个字母,替换为a.

​ 解码结果:“cba”.

[样例II] s = s= s=“cac”

​ ①第一个c字母最后一次出现的位置是 s [ 2 ] s[2] s[2],是最后一个字母,替换为a.

​ ②a字母最后一次出现的位置是 s [ 1 ] s[1] s[1],其后有c一个不同字母,故将其替换为第 ( 1 + 1 ) (1+1) (1+1)个字母,即b.

​ ③第二个c字母是最后一个字母,替换为a.

​ 解码结果:“aba”.

[样例III] s = s= s=“aacc”

​ ①第一个a字母最后一次出现的位置是 s [ 1 ] s[1] s[1],其后有c一个不同字母,故将其替换为第 ( 1 + 1 ) (1+1) (1+1)个字母,即b.同理第二个a替换为b.

​ ②第一个c字母最后一次出现的位置是 s [ 3 ] s[3] s[3],是最后一个字母,故将第一、二个c替换为a.

​ 解码结果:“bbaa”.

思路

s s s反转变为 s ′ s' s,枚举 s ′ s' s的起点,从前往后扫一遍 s ′ s' s, s ′ s' s的每一个起点对应 s s s的一个非空前缀. s ′ s' s中字母 c h ch ch第一次出现的位置即 c h ch ch s s s中最后一次出现的位置,只需用set统计 s ′ s' s c h ch ch之前出现了几种字母,将结果存在map<char,char>中,最后将 s s s的对应前缀的字母替换为map中的字母,更新字典序最大的答案.因 s s s中的所有相同字母会被替换成同一字母,故每个前缀中每个字母只需更新一次.

s ′ [ 0 ] s'[0] s[0]为起点的长度为 n n n,以 s ′ [ 1 ] s'[1] s[1]为起点的长度为 ( n − 1 ) , ⋯   , (n-1),\cdots, (n1),, s ′ [ n − 1 ] s'[n-1] s[n1]为起点的长度为 1 1 1,故循环 n + ( n − 1 ) + ⋯ + 1 = n ( n + 1 ) 2 n+(n-1)+\cdots+1=\dfrac{n(n+1)}{2} n+(n1)++1=2n(n+1)次.同理替换 s s s的前缀的字符需 n ( n + 1 ) 2 \dfrac{n(n+1)}{2} 2n(n+1)次,总时间复杂度 O ( n 2 ) O(n^2) O(n2), n n n最大 1000 1000 1000,可过.

代码 -> 2021ICPC沈阳-F(模拟)

int n;  // 字符串长度
string s;
string ans = "";
set<char> chcnt;  // 统计字符出现的次数
map<char, char> change;  // 记录字符要替换为什么字符

int main() {
	cin >> n >> s;

	string ss = s; reverse(ss.begin(), ss.end());  // 将s倒置
	for (int i = 0; i < n; i++) {  // 枚举ss的起点
		chcnt.clear();
		for (int j = i; j < n; j++) {  // 从前往后扫一遍ss
			if (chcnt.count(ss[j])) continue;  // 更新过

			int cnt = chcnt.size();  // 之前出现过的不同字母的数量
			change[ss[j]] = char('a' + cnt);
			chcnt.insert(ss[j]);  // 注意更新完cnt后再将该字符插入集合
		}
		string tmp = s.substr(0, n - i);  // 取出s中对应的前缀
		for (auto& item : tmp) item = change[item];  // 注意取引用
		ans = max(ans, tmp);
	}
	cout << ans;
}


J. Luggage Lock

题意

有一个四位数密码锁,每次操作可将一段连续的数字同时向上或向下转动一格.有 T    ( 1 ≤ T ≤ 1 e 5 ) T\ \ (1\leq T\leq 1\mathrm{e}5) T  (1T1e5)组询问,每组询问输入一行,包含两个密码状态 a 0 a 1 a 2 a 3 a_0a_1a_2a_3 a0a1a2a3 b 0 b 1 b 2 b 3 b_0b_1b_2b_3 b0b1b2b3.求将前者转化为后者的最少操作次数.

思路

显然BFS,从 a 0 a 1 a 2 a 3 a_0a_1a_2a_3 a0a1a2a3开始搜,时间复杂度 O ( 1 0 4 ) O(10^4) O(104),而 T T T最大 1 e 5 1\mathrm{e}5 1e5,会T.

注意到从一个状态转移到另一个状态只与两状态的数码的差值有关,则可将所有状态化为从 0000 0000 0000开始的状态.设 a 0 a 1 a 2 a 3 a_0a_1a_2a_3 a0a1a2a3转化为 0000 0000 0000至少需 x x x次操作, b 0 b 1 b 2 b 3 b_0b_1b_2b_3 b0b1b2b3转化为 0000 0000 0000至少需 y y y次操作,则最终答案 a n s = ∣ x − y ∣ ans=|x-y| ans=xy.

预处理出从 0000 0000 0000开始能转移到的状态的最小操作次数,共 1 e 4 1\mathrm{e}4 1e4种状态.每组测试样例 O ( 1 ) O(1) O(1)查询,总时间复杂度 O ( 1 e 4 ) O(1\mathrm{e}4) O(1e4).

每个状态用字符串表示.BFS的过程:枚举字符串的后缀的起点,得到这段后缀同时的 + 1 +1 +1 − 1 -1 1的状态,若状态未搜过,则入队并记录步数.

代码 -> 2021ICPC沈阳-J(BFS)

map<string, int> state;  // 记录状态及其步数

void bfs() {
	queue<string> que;
	que.push("0000"), state["0000"] = 0;  // 起点
	while (que.size()) {
		auto u = que.front(); que.pop();
		for (int i = 0; i < 4; i++) {  // 枚举字符串后缀的起点
			for (int j = i; j < 4; j++) {
				string up = u, down = u;  // 记录+1、-1的结果
				for (int k = i; k <= j; k++) {
					up[k] = '0' + (u[k] - '0' + 1) % 10;  // +1
					down[k] = '0' + (u[k] - '0' + 9) % 10;  // -1,其中+9即-1+10
				}
				if (!state.count(up)) {
					que.push(up);
					state[up] = state[u] + 1;  // 更新步数
				}
				if (!state.count(down)) {
					que.push(down);
					state[down] = state[u] + 1;  // 更新步数
				}
			}
		}
	}
}

int main() {
	bfs();

	CaseT{
		string a,b; cin >> a >> b;
		for (int i = 0; i < 4; i++) a[i] = '0' + (a[i] - b[i] + 10) % 10;
		cout << state[a] << endl;
	}
}


B. Bitwise Exclusive-OR Sequence

题意

现有 n    ( 1 ≤ n ≤ 1 e 5 ) n\ \ (1\leq n\leq 1\mathrm{e}5) n  (1n1e5)个非负数 a 1 , ⋯   , a n a_1,\cdots,a_n a1,,an,用 m    ( 0 ≤ m ≤ 2 e 5 ) m\ \ (0\leq m\leq 2\mathrm{e}5) m  (0m2e5)个限制 a u   x o r   a v = w a_u\ xor\ a_v=w au xor av=w描述.若满足限制的序列存在,输出 a i    ( i = 1 , ⋯   , n ) a_i\ \ (i=1,\cdots,n) ai  (i=1,,n)之和的最小值;否则输出 − 1 -1 1.

思路I By : yezzz.

a u   x o r   a v = w a_u\ xor\ a_v=w au xor av=w,则在节点 u u u v v v间连一条权值为 w w w的无向边.问题转化为求各节点点权之和的最小值.

注意到 m m m条限制不能保证所连成的图是树,则可能出现重边和环:①出现重边时,若两重边权值不同,则无解;②出现环时,以三元环为例,设三个节点分别为 A A A B B B C C C,且 A B AB AB B C BC BC C A CA CA的边权分别为 w 1 , w 2 , w 3 w_1,w_2,w_3 w1,w2,w3.若满足限制的序列存在,应满足 w 1   ∧ w 2   ∧ w 3 = A   ∧ B   ∧ B   ∧ C   ∧ C   ∧ A = 0 w_1\ ^\wedge w_2\ ^\wedge w_3=A\ ^\wedge B\ ^\wedge B\ ^\wedge C\ ^\wedge C\ ^\wedge A=0 w1 w2 w3=A B B C C A=0,即环上的每个节点都会被异或两次.

遇到重边和环即判断是否无解,剩下的都是链.考察链 A 1 A 2 ⋯ A n A_1A_2\cdots A_n A1A2An,设 A 1   ∧ A 2 = w 1 , A 2   ∧ A 3 = w 2 , ⋯   , A n − 1   ∧ A n = w n − 1 A_1\ ^\wedge A_2=w_1,A_2\ ^\wedge A_3=w_2,\cdots,A_{n-1}\ ^\wedge A_n=w_{n-1} A1 A2=w1,A2 A3=w2,,An1 An=wn1.注意到 A 2 = w 1   ∧ A 1 , A 3 = w 2   ∧ A 2 = ( w 1   ∧ w 2 )   ∧ A 1 , ⋯   , A n = ( w 1   ∧ w 2   ∧ ⋯   ∧ w n − 1 )   ∧ A 1 A_2=w_1\ ^\wedge A_1,A_3=w_2\ ^\wedge A_2=(w_1\ ^\wedge w_2)\ ^\wedge A_1,\cdots,A_n=(w_1\ ^\wedge w_2\ ^\wedge \cdots\ ^\wedge w_{n-1})\ ^\wedge A_1 A2=w1 A1,A3=w2 A2=(w1 w2) A1,,An=(w1 w2  wn1) A1,即 A 2 , ⋯   , A n A_2,\cdots,A_n A2,,An A 1 A_1 A1唯一确定.而每一位的异或值仅与两数的该位数码是否相同有关,则可枚举 A 1 A_1 A1的二进制表示的数位.遍历链时维护链上的节点的前缀异或和,确定 A 1 A_1 A1后用其快速求出 A 2 , ⋯   , A n A_2,\cdots,A_n A2,,An.

DFS.注意到确定节点 u u u的点权后,与其连通的所有节点的点权也唯一确定,则DFS维护 a 1 , ⋯   , a n a_1,\cdots,a_n a1,,an分别所在的连通块,每次遍历以节点 c e n t e r center center为中心的菊花图.若 c e n t e r center center的某条出边的终点 v v v已被遍历过,则出现重边或自环,如下图:

在这里插入图片描述
在这里插入图片描述

a 1 , ⋯   , a n a_1,\cdots,a_n a1,,an分别所在的连通块,考察它对答案的贡献.设第 p o s pos pos个连通块共 t o t tot tot个节点.枚举 A 1 A_1 A1的数位,统计 t o t tot tot个节点中对应数位为 1 1 1的个数 c n t cnt cnt.若 t o t tot tot个节点中该数位 1 1 1的数量比 0 0 0的数量多,为使 a i    ( i = 1 , ⋯   , n ) a_i\ \ (i=1,\cdots,n) ai  (i=1,,n)的总和最小, A 1 A_1 A1的该数位应取 1 1 1,否则取 0 0 0.确定 A 1 A_1 A1的值后,用前缀异或和求出 A 2 , ⋯   , A n A_2,\cdots,A_n A2,,An,它们之和即该连通块对答案的贡献.

菊花图的遍历还可用BFS.

代码I -> 2021ICPC沈阳-B(DFS)

const int MAXN = 1e5 + 5;
int n, m;  // 节点数、限制数
vii graph[MAXN];  // graph[u]={v,w}
bool vis[MAXN];  // 记录每个节点是否已被遍历过
ll pre[MAXN];  // 每条链上的前缀异或和
vi to[MAXN];  // 与每个节点连通的点

void dfs(int pos, int center) {  // 遍历到第pos个节点所在的连通块,当前菊花图的中心为center
	vis[center] = true;
	for (auto item : graph[center]) {  // 枚举中心节点的所有出边
		int v = item.first, w = item.second;
		if (vis[v]) {  // 重边或成环
			if ((pre[center] ^ w) != pre[v]) {  // 检查边权异或值是否矛盾,注意位运算符优先级低
				cout << -1;
				exit(0);
			}
		}
		else {
			pre[v] = pre[center] ^ w;  // 维护前缀异或和
			to[pos].push_back(v);  // 该点可与第pos个节点连通
			dfs(pos, v);  // 搜以节点v为中心的菊花图
		}
	}
}

ll cal(int pos) {  // 计算第pos个节点所在的连通块对答案的贡献
	int tot = to[pos].size();  // 第pos个节点所在的连通块的节点数
	int a1 = 0;
	for (int i = 0; i < 30; i++) {  // 枚举a1的数位
		int cnt = 0;  // 统计与第pos个节点连通的节点的点权中第i位为1的节点数
		for (auto v : to[pos]) // 枚举与第pos个节点连通的节点
			if ((pre[v] >> i) & 1) cnt++;
		if (cnt > tot - cnt) a1 += (1 << i);  // 若1比0多,则a1的第i位取1可使总和最小
	}
	ll res = a1;
	for (auto v : to[pos]) res += pre[v] ^ a1;
	return res;
}

int main() {
	cin >> n >> m;
	while (m--) {
		int u, v, w; cin >> u >> v >> w;
		graph[u].push_back(make_pair(v, w)), graph[v].push_back(make_pair(u, w));
	}

	ll ans = 0;
	for (int i = 1; i <= n; i++) {
		if (!vis[i]) {
			dfs(i, i);  // 遍历到第i个节点所在的连通块,当前菊花图的中心为第i个节点
			ans += cal(i);  // 计算第i个节点所在的连通块对答案的贡献
		}
	}
	cout << ans;
}

代码II -> 2021ICPC沈阳-B(BFS)

const int MAXN = 1e5 + 5;
int n, m;  // 节点数、限制数
vii graph[MAXN];  // graph[u]={v,w}
bool vis[MAXN];  // 记录每个节点是否已被遍历过
ll pre[MAXN];  // 每条链上的前缀异或和
vi to[MAXN];  // 与每个节点连通的点

void bfs(int pos) {  // 遍历到第pos个连通块
	qi que;
	que.push(pos);
	vis[pos] = true;
	while (que.size()) {
		int u = que.front(); que.pop();
		for (auto item : graph[u]) {  // 枚举u的所有出边
			int v = item.first, w = item.second;
			if (vis[v]) {  // 出现重边或自环
				if ((pre[u] ^ w) != pre[v]) {  // 检查边权异或值是否矛盾
					cout << -1;
					exit(0);
				}
			}
			else {
				vis[v] = true;
				pre[v] = pre[u] ^ w;
				to[pos].push_back(v);
				que.push(v);
			}
		}
	}
}

ll cal(int pos) {  // 计算第pos个节点所在的连通块对答案的贡献
	int tot = to[pos].size();  // 第pos个节点所在的连通块的节点数
	int a1 = 0;
	for (int i = 0; i < 30; i++) {  // 枚举a1的数位
		int cnt = 0;  // 统计与第pos个节点连通的节点的点权中第i位为1的节点数
		for (auto v : to[pos]) // 枚举与第pos个节点连通的节点
			if ((pre[v] >> i) & 1) cnt++;
		if (cnt > tot - cnt) a1 += (1 << i);  // 若1比0多,则a1的第i位取1可使总和最小
	}
	ll res = a1;
	for (auto v : to[pos]) res += pre[v] ^ a1;
	return res;
}

int main() {
	cin >> n >> m;
	while (m--) {
		int u, v, w; cin >> u >> v >> w;
		graph[u].push_back(make_pair(v, w)), graph[v].push_back(make_pair(u, w));
	}

	ll ans = 0;
	for (int i = 1; i <= n; i++) {
		if (!vis[i]) {
			bfs(i);  // 遍历第i个节点所在的连通块
			ans += cal(i);  // 计算第i个节点所在的联通块对答案的贡献
		}
	}
	cout << ans;
}

DFS:

在这里插入图片描述

BFS:

在这里插入图片描述


思路II By : AC__dream

对边权的每个数位维护一个连通块,其中边权的二进制表示中对应数位为 1 1 1时连一条边权为 1 1 1的边,否则连一条边权为 0 0 0的边.因 w < 2 30 w<2^{30} w<230,则图中有 30 30 30个连通块,分别对它们染色,每个节点染 1 1 1 0 0 0,则每个连通块中 1 1 1的个数的最小值乘对应的权值即为该数位对答案的贡献.

1 1 1的个数不确定的原因:将一张已染色的图颜色反转后仍满足条件.

代码III -> 2021ICPC沈阳-B(染色)

const int MAXN = 1e5 + 5, MAXM = 2e5 + 5;
int n, m;  // 节点数、限制数
int head[30][MAXN], nxt[30][MAXM << 1], val[30][MAXM << 1], weight[30][MAXM << 1], idx[MAXM << 1];  // 至多有30个连通块
int color[30][MAXN];
bool vis[30][MAXN];
int cnt = 0;  // 统计连通块中的节点数

void add(int pos, int u, int v, int w) {  // 第pos个连通块中建u<->v的权值为w的双向边
	val[pos][idx[pos]] = v;
	weight[pos][idx[pos]] = w;
	nxt[pos][idx[pos]] = head[pos][u];
	head[pos][u] = idx[pos]++;
}

int dfs(int pos, int center, int c) {  // 将第pos个连通块中以center为中心的菊花图染成c色,返回连通块中1的个数
	color[pos][center] = c;
	int res = c;  // 统计第pos个连通块中1的个数
	for (int u = head[pos][center]; ~u; u = nxt[pos][u]) {
		int v = val[pos][u];
		if (vis[pos][v]) {  // 出现重边或自环
			if ((c ^ weight[pos][u]) != color[pos][v]) {  // 检查是否矛盾
				cout << -1;
				exit(0);
			}
		}
		else {
			cnt++;  // 更新该连通块中的节点数
			vis[pos][v] = true;
			res += dfs(pos, v, c ^ weight[pos][u]);  // 染以v为中心的菊花图
		}
	}
	return res;
}

ll cal(int pos) {  // 求第pos个连通块对答案的贡献
	ll res = 0;
	for (int i = 1; i <= n; i++) {
		if (vis[pos][i]) continue;

		vis[pos][i] = true;
		cnt = 1;  // 用于DFS时统计连通块中的节点数
		int tmp = dfs(pos, i, 1);  // 将连通块中的第一个节点染成1得到的整个连通块中1的个数
		res += min(tmp, cnt - tmp);  // cnt-tmp是将连通块的第一个节点染成0得到的连通块中1的个数
	}
	return res;
}

int main() {
	memset(head, -1, so(head));
	
	cin >> n >> m;
	while (m--) {
		int u, v, w; cin >> u >> v >> w;
		for (int i = 0; i < 30; i++) {  // 每个数位维护一个连通块
			if (w >> i & 1) add(i, u, v, 1), add(i, v, u, 1);  // 建边权为1的边
			else add(i, u, v, 0), add(i, v, u, 0);  // 建边权为0的边
		}
	}

	ll ans = 0;
	for (int i = 0; i < 30; i++) ans += cal(i) * (1 << i);  // 统计每个连通块对答案的贡献
	cout << ans;
}

在这里插入图片描述


思路III By : MoYan1082)

a u   ∧ a v = w a_u\ ^\wedge a_v=w au av=w,考察 w w w的每个数位:①若该数位为 1 1 1,则 a u , a v a_u,a_v au,av的该数位不同;②若该数位为 0 0 0,则 a u , a v a_u,a_v au,av的该数位相同.

a u , a v a_u,a_v au,av的对应数位的异同分为两类,用带扩展域的并查集维护:①若 a u a_u au a v a_v av的对应数位相同,则合并 f a [ u ] fa[u] fa[u] f a [ v ] , f a [ u + n ] fa[v],fa[u+n] fa[v],fa[u+n] f a [ v + n ] fa[v+n] fa[v+n];②若 a u a_u au a v a_v av的对应数位不同,则合并 f a [ u ] fa[u] fa[u] f a [ v + n ] , f a [ u + n ] fa[v+n],fa[u+n] fa[v+n],fa[u+n] f a [ v ] fa[v] fa[v].

代码IV -> 2021ICPC沈阳-B(并查集)

const int MAXN = 2e5 + 10;
int n, m;  // 节点数、限制数
struct Edge {
	int u, v, w;
}edges[MAXN];
int fa[MAXN], siz[MAXN];  // 并查集的fa数组、集合的大小

int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); }

void init() {  // 初始化并查集
	for (int i = 1; i <= n; i++) {
		fa[i] = i, fa[i + n] = i + n;
		siz[i] = 1, siz[i + n] = 0;
	}
}

int main() {
	cin >> n >> m;
	for (int i = 1; i <= m; i++) cin >> edges[i].u >> edges[i].v >> edges[i].w;

	ll ans = 0;
	for (int i = 0; i < 30; i++) {  // 枚举每个数位
		init();

		for (int j = 1; j <= m; j++) {
			int tmpu = find(edges[j].u), tmpv = find(edges[j].v);
			int tmpun = find(edges[j].u + n), tmpvn = find(edges[j].v + n);

			if ((edges[j].w >> i) & 1) {  // 该位为1,说明对应位数码不同
				if (tmpu == tmpv) {  // 矛盾
					cout << -1;
					exit(0);
				}

				if (tmpu == tmpvn) continue;

				// 合并tmpu和tmpvn,tmpv和tmpun
				fa[tmpvn] = tmpu, siz[tmpu] += siz[tmpvn];
				fa[tmpun] = tmpv, siz[tmpv] += siz[tmpun];
			}
			else {  // 该位为0,说明对应位数码相同
				if (tmpu == tmpvn) {  // 矛盾
					cout << -1;
					exit(0);
				}

				if (tmpu == tmpv) continue;

				// 合并tmpu和tmpv,tmpvn和tmpun
				fa[tmpu] = tmpv, siz[tmpv] += siz[tmpu];
				fa[tmpun] = tmpvn, siz[tmpvn] += siz[tmpun];
			}
		}

		for (int j = 1; j <= n; j++) {  // 统计每个节点对答案该数位的贡献
			ans += (ll)min(siz[find(j)], siz[find(j + n)]) * (1 << i);
			siz[find(j)] = siz[find(j + n)] = 0;
		}
	}
	cout << ans;
}

在这里插入图片描述



I. Linear Fractional Transformation ( 2   s 2\ \mathrm{s} 2 s)

题意

T    ( 1 ≤ T ≤ 1 e 5 ) T\ \ (1\leq T\leq 1\mathrm{e}5) T  (1T1e5)组测试数据.每组测试数据给定复变函数 f ( z ) = a z + b c z + d    ( a , b , c , d ∈ C , a d − b c ≠ 0 ) f(z)=\dfrac{az+b}{cz+d}\ \ (a,b,c,d\in\mathbb{C},ad-bc\neq 0) f(z)=cz+daz+b  (a,b,c,dC,adbc=0)经过的不同的三点 ( z i , w i )    ( i = 1 , 2 , 3 ) (z_i,w_i)\ \ (i=1,2,3) (zi,wi)  (i=1,2,3).给定 z 0 z_0 z0,求 f ( z 0 ) f(z_0) f(z0).数据保证有唯一解,且 ∣ f ( z 0 ) ∣ < 1 e 6 |f(z_0)|<1\mathrm{e}6 f(z0)<1e6;出现的所有复数的实部、虚部的绝对值都不超过 100 100 100.

有唯一解的解释

分式线性变换是齐次坐标的基变换.在实射影平面 π ‾ \overline{\pi} π上取基底 e 1 → , e 2 → \overrightarrow{e_1},\overrightarrow{e_2} e1 ,e2 ,取 π ‾ \overline{\pi} π上一点 O ′ O' O π ‾ \overline{\pi} π外一点 O O O,则以 [ O O ′ → , e 1 → , e 2 → ] [\overrightarrow{OO'},\overrightarrow{e_1},\overrightarrow{e_2}] [OO ,e1 ,e2 ]为基底的坐标称为齐次坐标.射影平面上将直线视为点,则若 ( x 1 , x 2 , x 3 ) ∈ π ‾ (x_1,x_2,x_3)\in\overline{\pi} (x1,x2,x3)π,显然 λ ( x 1 , x 2 , x 3 ) ∈ π ‾    ( λ ≠ 0 ) \lambda(x_1,x_2,x_3)\in\overline{\pi}\ \ (\lambda\neq 0) λ(x1,x2,x3)π  (λ=0),即齐次坐标只考虑方向,不考虑大小.令 λ = 1 x 3 \lambda=\dfrac{1}{x_3} λ=x31,则 ( x 1 , x 2 , x 3 ) ∼ ( x 1 x 3 , x 2 x 3 , 1 ) (x_1,x_2,x_3)\sim \left(\dfrac{x_1}{x_3},\dfrac{x_2}{x_3},1\right) (x1,x2,x3)(x3x1,x3x2,1),即 R 2 \mathbb{R}^2 R2上的坐标 ( x , y ) (x,y) (x,y)与齐次坐标 ( x , y , 1 ) (x,y,1) (x,y,1)构成双射.同理, C \mathbb{C} C上的点 z z z与齐次坐标 ( z , 1 ) (z,1) (z,1)构成双射.

思路I

c = 0 c=0 c=0时, f ( z ) = a z + b d f(z)=\dfrac{az+b}{d} f(z)=daz+b.令 d = 1 d=1 d=1,则 f ( z ) = a z + b f(z)=az+b f(z)=az+b,两个未知数,代入 ( z 1 , w 1 ) , ( z 2 , w 2 ) (z_1,w_1),(z_2,w_2) (z1,w1),(z2,w2)得: { z 1 a + b = w 1 z 2 a + b = w 2 \begin{cases}z_1a+b=w_1 \\ z_2a+b=w_2\end{cases} {z1a+b=w1z2a+b=w2,解得: { a = w 1 − w 2 z 1 − z 2 b = w 1 − z 1 a \begin{cases}a=\dfrac{w_1-w_2}{z_1-z_2} \\ b=w_1-z_1a\end{cases} a=z1z2w1w2b=w1z1a.因已知条件是 ( z i , w i ) (z_i,w_i) (zi,wi)而非系数,则通过 z 3 a + b − w 3 z_3a+b-w_3 z3a+bw3是零向量来判定 c = 0 c=0 c=0.

c ≠ 0 c\neq 0 c=0时,令 c = 1 c=1 c=1,则 f ( z ) = a z + b z + d f(z)=\dfrac{az+b}{z+d} f(z)=z+daz+b,三个未知数,代入 ( z i , w i )    ( i = 1 , 2 , 3 ) (z_i,w_i)\ \ (i=1,2,3) (zi,wi)  (i=1,2,3)得: { z 1 a + b − w 1 d = w 1 z 1 z 2 a + b − w 2 d = w 2 z 2 z 3 a + b − w 3 d = w 3 z 3 \begin{cases}z_1a+b-w_1d=w_1z_1 \\ z_2a+b-w_2d=w_2z_2 \\ z_3a+b-w_3d=w_3z_3\end{cases} z1a+bw1d=w1z1z2a+bw2d=w2z2z3a+bw3d=w3z3,用Gauss消元求出 a , b , d a,b,d a,b,d后,代入 z 0 z_0 z0求出 f ( z 0 ) f(z_0) f(z0).

总时间复杂度 O ( T n 3 ) O(Tn^3) O(Tn3),其中 n = 3 , T n=3,T n=3,T最大 1 e 5 1\mathrm{e}5 1e5,最坏 2.7 e 6 2.7\mathrm{e}6 2.7e6,但常数大.

每组测试数据要输入 14 14 14个数,则共需输入 1.4 e 6 1.4\mathrm{e}6 1.4e6个数,要用scanf读,实测cin会T.

代码I -> 2021ICPC沈阳-I(Gauss消元)

cp A[5][5];  // 线性方程组的增广矩阵
cp z[5], w[5];  // z_i,w_i
cp z0;  // z_0

double get_length2(cp x) { return x.real() * x.real() + x.imag() * x.imag(); }  // 求复数模长的平方

void gauss(int n){  // Gauss消元
	for (int i = 1; i <= n; i++) {
		int row = i;
		for (int j = i + 1; j <= n; j++)  // 取出主元最大的一行
			if (get_length2(A[j][i]) > get_length2(A[row][i])) row = j;
		if (row != i) swap(A[row], A[i]);  // 将主元最大的行换到上面
		
		if (get_length2(A[i][i]) < eps) continue;  // 无需消元

		for (int j = 1; j <= n; j++) {
			if (i == j) continue;

			// 消元
			cp tmp = A[j][i] / A[i][i];
			for (int k = i; k <= n + 1; k++) A[j][k] -= A[i][k] * tmp;
		}
	}

	for (int i = 1; i <= n; i++) {
		if (get_length2(A[i][i]) < eps) continue;

		A[i][n + 1] /= A[i][i];
	}
}

int main() {
	CaseT{
		double x,y;
		for (int i = 1; i <= 3; i++) {
			scanf("%lf%lf", &x, &y); z[i] = { x,y };
			scanf("%lf%lf", &x, &y); w[i] = { x,y };
		}
		scanf("%lf%lf", &x, &y); z0 = { x,y };

		cp a = (w[1] - w[2]) / (z[1] - z[2]), b = w[1] - a * z[1], ans = 0;
		if (get_length2(a * z[3] + b - w[3]) < eps) ans = a * z0 + b;
		else {
			for (int i = 1; i <= 3; i++) 
				A[i][1] = z[i], A[i][2] = { 1,0 }, A[i][3] = -w[i], A[i][4] = w[i] * z[i];
			gauss(3);
			ans = (A[1][4] * z0 + A[2][4]) / (z0 + A[3][4]);
		}
		printf("%.12lf %.12lf\n", ans.real(), ans.imag());
	}
}

在这里插入图片描述


思路II By : 工口发动机)

f ( z i ) = w i    ( i = 0 , 1 , 2 , 3 ) f(z_i)=w_i\ \ (i=0,1,2,3) f(zi)=wi  (i=0,1,2,3).由分式线性变换保交比知: ( z 0 , z 1 , z 2 , z 3 ) = ( w 0 , w 1 , w 2 , w 3 ) (z_0,z_1,z_2,z_3)=(w_0,w_1,w_2,w_3) (z0,z1,z2,z3)=(w0,w1,w2,w3),其中 ( a , b , c , d ) = c − a c − b : d − a d − b (a,b,c,d)=\dfrac{c-a}{c-b}:\dfrac{d-a}{d-b} (a,b,c,d)=cbca:dbda,则 w 2 − w w 2 − w 1 ⋅ z 3 − z z 3 − z 1 = w 3 − w w 3 − w 1 ⋅ z 3 − z z 3 − z 1 \dfrac{w_2-w}{w_2-w_1}\cdot\dfrac{z_3-z}{z_3-z_1}=\dfrac{w_3-w}{w_3-w_1}\cdot\dfrac{z_3-z}{z_3-z_1} w2w1w2wz3z1z3z=w3w1w3wz3z1z3z.

k 1 = ( z 0 − z 1 ) ( w 3 − w 1 ) ( z 3 − z 2 ) , k 2 = ( z 0 − z 2 ) ( w 3 − w 2 ) ( z 3 − z 1 ) k_1=(z_0-z_1)(w_3-w_1)(z_3-z_2),k_2=(z_0-z_2)(w_3-w_2)(z_3-z_1) k1=(z0z1)(w3w1)(z3z2),k2=(z0z2)(w3w2)(z3z1),整理得 w 0 = k 2 w 1 − k 1 w 2 k 2 − k 1 w_0=\dfrac{k_2w_1-k_1w_2}{k_2-k_1} w0=k2k1k2w1k1w2.

代码II -> 2021ICPC沈阳-I(推公式)

cp z[5], w[5];  // z_i,w_i
cp z0;  // z_0

int main() {
	CaseT{
		double x,y;
		for (int i = 1; i <= 3; i++) {
			scanf("%lf%lf", &x, &y); z[i] = { x,y };
			scanf("%lf%lf", &x, &y); w[i] = { x,y };
		}
		scanf("%lf%lf", &x, &y); z0 = { x,y };

		cp k1 = (z0 - z[1]) * (w[3] - w[1]) * (z[3] - z[2]), k2 = (z0 - z[2]) * (w[3] - w[2]) * (z[3] - z[1]);
		cp ans = (k2 * w[1] - k1 * w[2]) / (k2 - k1);
		printf("%.12lf %.12lf\n", ans.real(), ans.imag());
	}
}

在这里插入图片描述



M. String Problem ( 2   s 2\ \mathrm{s} 2 s)

题意

给定一个长度不超过 1 e 6 1\mathrm{e}6 1e6的、只包含 26 26 26个小写字母的字符串,对每个非空前缀,求其中字典序最大的子串,输出其左右端点在原串中的下标,字符串下标从 1 1 1开始.

思路

观察样例知:第二列是 1 , 2 , 3 , 4 , 5 , ⋯ 1,2,3,4,5,\cdots 1,2,3,4,5,,容易想到每个前缀中字典序最大的子串的右端点即该前缀的右端点,若不然,设该前缀是 s t r [ 1 , n ] str[1,n] str[1,n],该前缀中字典序最大的子串是 s [ l , r ] s[l,r] s[l,r].考察子串 s [ l , n ] s[l,n] s[l,n],它与 s [ l , r ] s[l,r] s[l,r]前缀相同,但长度 > s [ l , r ] >s[l,r] >s[l,r],故 s [ l , n ] s[l,n] s[l,n]的字典序更大,与 s [ l , r ] s[l,r] s[l,r]的字典序最大矛盾.

先写出 O ( n 2 ) O(n^2) O(n2)暴力做法:

cin >> str; n = str.length();

for (int right = 0; right < n; right++) {  // 枚举右端点
  int left;  // 左端点
  string maxstr = "";
  for (int i = 0; i <= right; i++) {
    string tmp = str.substr(i, right - i + 1);
    if (tmp > maxstr) {
      maxstr = tmp;
      left = i;
    }
  }
  cout << left + 1 << ' ' << right + 1 << endl;  // 题目下标从1开始
}

在这里插入图片描述

考虑如何优化.如上图,设 c h 2 ch_2 ch2 c h 1 ch_1 ch1之后第一个 > c h 1 >ch_1 >ch1的字符,则 c h 1 ch_1 ch1 c h 2 ch_2 ch2之间的 s t r str str的所有前缀中字典序最大的子串都是以 c h 1 ch_1 ch1开头的,即红色部分的答案的左端点相同,记录后直接跳到下一段的开头 c h 2 ch_2 ch2位置,这样 s t r str str只会被遍历一次,时间复杂度 O ( n ) O(n) O(n).该情况 i i i移动到下一段的开头即 i = k i=k i=k.

在这里插入图片描述

若遍历到与首字符相同的字符,则需比较前缀来确定哪个子串是字典序最大的.该情况 i i i移动到下一段的开头是第二个 c h 1 ch_1 ch1的位置,即 i = k − ( j − i ) i=k-(j-i) i=k(ji),上述情况也可归为该情况.

注意两种情况 i i i都需移动到 j j j之后,才能保证字符串仅被遍历一次,进而保证 O ( n ) O(n) O(n)的时间复杂度.

注意答案没有被更新过才需要更新,因为之前更新出的答案是子串中最靠左的.

代码 -> 2021ICPC沈阳-M(模拟)

const int MAXN = 1e6 + 5;
string str;
int n;  // 字符串长度
int ans[MAXN];  // 记录字典序最大的子串的左端点

int main() {
	cin >> str; str = " " + str; n = str.length() - 1;

	int i = 1;
	while (i <= n) {
		if(!ans[i]) ans[i] = i;  // 该位置的字典序最大的子串为str[i,i]
		int j = i, k = i + 1;
		while (k <= n && str[k] <= str[j]) {
			if(!ans[k]) ans[k] = i;
			if (str[k] == str[j]) j++;  // 首字符相同,比较第二个字符
			else j = i;
			k++;
		}
		while(i <= j) i = k - (j - i);  // i移到j之后的下一段的开头
	}

	for (int i = 1; i <= n; i++) cout << ans[i] << ' ' << i << endl;
}

在这里插入图片描述



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值