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 (1≤n≤1000)、只包含前 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, (n−1),⋯,以 s ′ [ n − 1 ] s'[n-1] s′[n−1]为起点的长度为 1 1 1,故循环 n + ( n − 1 ) + ⋯ + 1 = n ( n + 1 ) 2 n+(n-1)+\cdots+1=\dfrac{n(n+1)}{2} n+(n−1)+⋯+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 (1≤T≤1e5)组询问,每组询问输入一行,包含两个密码状态 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=∣x−y∣.
预处理出从 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 (1≤n≤1e5)个非负数 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 (0≤m≤2e5)个限制 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 A1A2⋯An,设 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,⋯,An−1 ∧An=wn−1.注意到 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 ∧⋯ ∧wn−1) ∧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 (1≤T≤1e5)组测试数据.每组测试数据给定复变函数 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,d∈C,ad−bc=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=z1−z2w1−w2b=w1−z1a.因已知条件是 ( z i , w i ) (z_i,w_i) (zi,wi)而非系数,则通过 z 3 a + b − w 3 z_3a+b-w_3 z3a+b−w3是零向量来判定 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+b−w1d=w1z1z2a+b−w2d=w2z2z3a+b−w3d=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)=c−bc−a:d−bd−a,则 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} w2−w1w2−w⋅z3−z1z3−z=w3−w1w3−w⋅z3−z1z3−z.
令 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=(z0−z1)(w3−w1)(z3−z2),k2=(z0−z2)(w3−w2)(z3−z1),整理得 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=k2−k1k2w1−k1w2.
代码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−(j−i),上述情况也可归为该情况.
注意两种情况 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;
}