2021 沈阳 ICPC 区域赛

rank 链接

签到:EF

铜牌题:BJ

银牌题:HILM

金牌题:G...

B Bitwise Exclusive-OR Sequence

\(n\) 个数,\(m\) 个关系,每个关系形如 \(a_u\oplus a_v = w\),表示第 \(u\) 个数与第 \(v\) 数的异或运算结果为 \(w\)。求是否有这样的\(n\) 个数满足所有关系要求,如果没有输出 -1,如果有输出所有满足要求的方案中,所有数字的和的最小值。

\(n \le 100000,m\le 200000,w\lt 2^{20}\)


可以看做 \(n\) 个点,\(m\) 条边的无向图,每个连通块内都通过一些关系使得两两可达。所以,如果连通块内的一个数字确定,那么所有的数字都可以确定下来。

可以选择连通块中的某个点作为根 \(rt\),那么可以使用 DFS 求出整个连通块中所有点与 \(rt\) 的异或值。这个过程可能会检测出问题无解的情况。由此,可以按 30 位去考虑,计算出整个连通块中每个点与 \(rt\) 异或值的每一位为 0 和 1 的个数,然后选择较小的作为答案。为什么可以选择较小的,因为 \(rt\) 的实际值是由我们去确定的,假设第 \(i\) 位上,与 \(rt\) 异或为 0 的个数为 \(a\),为 1 的为 \(b\),假设 \(a<b\),那么可以让 \(rt\) 的实际值的第 \(i\) 位为 1,这样的话,连通块中只会产生 \(a*2^i\) 的贡献。复杂度为 \(O(30n)\)

也可以建 30 次图去跑,不过每次重新建图容易被卡常数。也可以每次考虑判定二分图,并利用并查集缩点,即异或为 0 的去合并,异或为 1 的连边,然后去判断二分图。实际测试起来也非常快。

#include<bits/stdc++.h>
using namespace std;
#define rep(i,j,k) for(int i=int(j);i<=int(k);i++)
#define per(i,j,k) for(int i=int(j);i>=int(k);i--)
typedef long long ll;
const int N = 100010, M = 400010;
int n, m, vis[N], val[N], cnt[32][2];
int head[N], ver[M], nxt[M], edge[M], tot;
bool flag;
void add(int x, int y, int z){
  ver[++tot] = y, nxt[tot] = head[x], edge[tot] = z, head[x] = tot;
}
void dfs(int x, int fa = 0) {
  if(!flag) return;
  vis[x] = 1;
  for(int i=30;i>=0;i--){
    cnt[i][val[x] >> i & 1] ++;
  }
  for(int i=head[x];i;i=nxt[i]) {
    int y = ver[i];
    if(vis[y]) {
      if((val[y] ^ val[x]) != edge[i]) {
        flag = false;
        return;
      }
    } else {
      val[y] = val[x] ^ edge[i];
      dfs(y, x);
    }
  }
}
int main(){
  scanf("%d%d", &n, &m);
  for(int i=1;i<=m;i++){
    int x, y, w;
    scanf("%d%d%d", &x, &y, &w);
    add(x, y, w); add(y, x, w);
  }
  flag = true;
  ll res = 0;
  for(int i=1;i<=n;i++){
    if(!vis[i]) {
      memset(cnt, 0, sizeof cnt);
      dfs(i);
      for(int j=30;j>=0;j--){
        res += 1ll * min(cnt[j][0], cnt[j][1]) * (1 << j);
      }
    }
  }
  if(!flag) {
    puts("-1"); return 0;
  }
  printf("%lld\n", res);
  return 0;
}

E Edward Gaming, the Champion

判断一个串中出现了多少次 "edgnb"


#include <bits/stdc++.h>
using namespace std;
const int N = 200010;
char s[N];
int main(){
    string s;
    cin >> s;
    int n = s.length();
    int res = 0;
    for(int i=0;i<=n-5;i++){
        if(s.substr(i, 5) == "edgnb") {
            res ++;
        }
    }
    cout << res << endl;
    return 0;
}

F Encoded Strings I

给定一个长度为 \(n(n\le 1000)\) 的字符串\(s\),定义函数 \(F_s\)

\(F_s(c) = chr(G(c,S))\) ,表示将 S 中的每个字符 c 转换为 \(chr(G(c,S))\),其中 \(G(c,S)\) 表示 S 中最后一次出现 c 之后的后缀中不同的字符个数,\(chr(i)\) 表示第 \(i\) 个字符(第个0字符是 a,第1个是 b...)。

要求你对 \(s\) 的每个非空前缀代入到函数中,得到 \(n\) 个字符串,输出按照字典序排序的最大字符串。


难在题意理解,字符串 \(aacc\) 有前缀 \(a,aa,aac,aacc\),它们代入 \(F_s\) 得到:\(a,aa,bba,bbaa\),答案是 \(bbaa\)

首先预处理出每种字符最后出现的位置,再预处理出每个后缀出现的字符个数,暴力计算即可。(实现方法有很多)

#include<bits/stdc++.h>
using namespace std;
#define rep(i,j,k) for(int i=int(j);i<=int(k);i++)
#define per(i,j,k) for(int i=int(j);i>=int(k);i--)
typedef long long ll;
const int N = 1010;
int n;
char s[N];
int last[26], suf[N];
string get(int n) {
  unordered_map<char, int> mp;
  for(int i=n;i>=1;i--){
    suf[i] = mp.size();
    mp[s[i]] ++;
  }
  for(int i=1;i<=n;i++){
    last[s[i] - 'a'] = i;
  }
  string t = "";
  for(int i=1;i<=n;i++){
    t += char('a' + suf[last[s[i] - 'a']]);
  }
  return t;
}
int main(){
  scanf("%d%s", &n, s + 1);
  string res = "";
  for(int i=1;i<=n;i++) {
    res = max(res, get(i));
  }
  cout << res << endl;
}

G Encoded Strings II

给定一个长度为 \(n(n\le 1000)\),且仅包含 \(a\sim t\) 的字符串,求对它的所有非空子序列使用 \(F_s\) 函数处理后,字典序最大的结果。


字符串最多包含 20 种字符,这个条件可以被我们所利用。

假如字符串只包含了\(k\) 种字符,那么最终答案肯定是 \(k\) 段连续字符,因为首个字符字典序要最大。

然后枚举 \(k\) 种字符,在子序列中出现的顺序,然后对于每一段字符,起始位置是固定的,尝试去找结束维护。

假设现在处理第 i 个字符,它代表了答案的第 \(i\) 段字符,由于希望答案尽可能的字典序更大,所以我们希望第 \(i\) 段字符也尽可能的长。

另外需要保证其他 \(k-i\) 种字符在后面可以出现,所以处理 \(f[s]\) 表示 \(k-i\) 种字符构成的二进制集合 \(s\),使得这 \(k\) 种字符都出现的最短后缀出现的位置。

现在有了第 \(i\) 段字符可选的区间范围 \(l,r\)\(r\) 对于每种字符可能不同,我们想找到这个范围里面,出现次数最多的字符,然后枚举它,继续递归。递归时,下次搜索的左端点是当前枚举字符在 \(r\) 之前最后一次出现的位置。

#include<bits/stdc++.h>
using namespace std;
#define rep(i,j,k) for(int i=int(j);i<=int(k);i++)
#define per(i,j,k) for(int i=int(j);i>=int(k);i--)
typedef long long ll;
const int N = 1005;
// sum 是前缀和,last 表示每个字符出现的最后位置,f[i] 表示 i 集合字符出现的最早后缀位置,pre[i][j] 表示 j 字符在 i 位置之前出现的最近位置。
int n, sum[N][20], last[20], f[1<<20], pre[N][20];
int cnt, num[20], vis[20];
char s[N];
void dfs(int u, int l) {
  if(u == cnt) return;
  int st = 0; // 找到还没有分配的字符集合 st
  for(int i=0;i<20;i++) if(!vis[i] && last[i]) st |= 1 << i;
  int mx = 0; // 在 l,r 中找到最多的出现次数
  for(int i=0;i<20;i++) {
    if(!vis[i] && last[i]) {
      int r = f[st ^ (1 << i)] - 1; // st 集合中除去 i, 剩下的字符都出现的最短后缀位置。
      mx = max(mx, sum[r][i] - sum[l-1][i]);
    }
  }
  if(mx < num[u]) return; // 剪枝,第 u 段长度不够更新
  if(mx > num[u]) { // 足够去更新第 u 段字符的长度,那么后面所有的长度都归 0
    num[u] = mx;
    for(int i=u+1;i<cnt;i++) num[i] = 0;
  }
  for(int i=0;i<20;i++) { // 枚举每个出现次数为 mx 的字符 i
    if(!vis[i] && last[i]) {
      int r = f[st ^ (1 << i)] - 1;
      int x = sum[r][i] - sum[l-1][i];
      if(x == mx) {
        vis[i] = 1;
        dfs(u + 1, pre[r][i] + 1); // 下次搜索的起点,应当是 r 之前最后一次出现 i 字符的后一个位置。
        vis[i] = 0;
      }
    }
  }
}
int main(){
  scanf("%d%s", &n, s + 1);
  // 预处理
  for(int i=1;i<=n;i++){
    memcpy(sum[i], sum[i-1], sizeof sum[i]);
    memcpy(pre[i], pre[i-1], sizeof pre[i]);
    int c = s[i] - 'a';
    sum[i][c] ++;
    pre[i][c] = i;
    if(!last[c]) cnt++;
    last[c] = i;
  }
  // 预处理 f[i]
  f[0] = n + 1;
  for(int i=0;i<(1<<20);i++) {
    for(int j=0;j<20;j++) {
      if(i >> j & 1) {
        f[i] = min(f[i ^ (1 << j)], last[j]);
        break;
      }
    }
  }
  dfs(0, 1);
  for(int i=0;i<cnt;i++) {
    for(int j=0;j<num[i];j++) putchar('a' + (cnt - i - 1));
  }
  puts("");
}

J Luggage Lock

长度为 4 的密码锁,每次可以选择连续的一段数字,向上或者向下拨动 1。求从 \(a_0a_1a_2a_3\) 转动到 \(b_0b_1b_2b_3\) 的最小步数。


正解是任何一组转移都可以转换成从 \(0000\) 开始转到 \(abcd\),这样可以 \(BFS\) 进行预处理然后 \(O(1)\) 回答。复杂度 \(O(T)\)。(每次转移考虑一个区间,向上或者向下转,共 20 种转移)

另外一种解法是考虑每个数字最终是向上转还是向下转(可以想想它不会既向上又向下),假设 \(a\)\(b\) 可以向上转 \(x\) 步,也可以向下转 \(10-x\) 步,但只考虑这两种转移,即考虑 \(2^4\) 种方案并不能AC,有些数字可能会多转一圈。(不过还没有找到特殊情况),对拍的话发现需要考虑向上转 \(x,10+x\),向上转 \(10-x,20-x\) 等四种方案,即 \(4^4=256\) 种方案,然后求最小值。复杂度 \(O(256T)\)

#include <bits/stdc++.h>
using namespace std;
int T;
char s[10], t[10];
int a[10][5], b[10];
int res;
int sgn(int x) {
	if (x == 0) return 0;
	return x > 0 ? 1 : -1;
}
void dfs(int x) {
	if (x == 5) {
		int ans = 0;
		for (int k = 1; k <= 4; k++)
		{
			if (b[k] == 0) continue;
			int p = 4;
			for (int j = k; j <= 4; j++)
				if (sgn(b[k]) != sgn(b[j])) { p = j - 1; break; }
			int last = 0;
			for (int c = k; c <= p; c++)
			{
				int kk = 0;
				if (b[c] < 0) kk = -b[c];
				else kk = b[c];
				if (kk >= last) ans += kk - last;
				last = kk;
			}
			k = p;
		}
		res = min(res, ans);
		return;
	}
	for (int i = 0; i < 4; i++) {
		b[x] = a[x][i];
		dfs(x + 1);
	}
}
int main() {
	scanf("%d", &T);
	while (T--) {
		res = 1e9;
		scanf("%s %s", s + 1, t + 1);
		for (int i = 1; i <= 4; i++) {
			int c = s[i] - t[i];
            if (c < 0) c = 10 + c;
            a[i][0] = c;
            a[i][1] = c - 10;
            a[i][2] = c + 10;
            a[i][3] = c - 20;
		}
		dfs(1);
		printf("%d\n", res);
	}
	return 0;
}

H Line Graph Matching

\(n\) 个点,\(m\) 条边的带权无向连通图,将其转换为线图即,原图中的点变成边,边变成点,如果原图中两个边有公共点,那么就在线图中对应的两个点连相应的带权值的边,权值为原图两个边的权值和。求线图中的最大权点匹配,即选出来一些边,它们没有公共点并且权值和最大。

\(n\le 10^5,m\le 2*10^5\)


最大权点匹配要求每两个点需要有公共边,线图中的点即原图中的边,所以原问题可以转换为最大权边匹配。只要有两个边有公共点,就可以匹配。

题目给定的是一个连通图,如果边数为偶数,那么一定可以全部匹配,如果边数为奇数,那么需要选择一条边删去。(假想的结论)

但是删除边是有要求的,如果要删的是割边,那么需要保证删除之后每个连通块边的数量都是偶数。如果删的不是割边,则不需要任何限制。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 100010, M = 400010;
int head[N], ver[M], nxt[M], edge[M], vis[N];
int dfn[N], low[N], cnt[N], n, m, tot, num;
int Min;
void add(int x, int y, int z) {
	ver[++tot] = y, edge[tot] = z,  nxt[tot] = head[x], head[x] = tot;
}
void tarjan(int x, int in_edge) {
	dfn[x] = low[x] = ++num;
	for (int i=head[x]; i; i = nxt[i]) {
		int y = ver[i];
		if (!dfn[y]) {
			tarjan(y, i);
			cnt[x] += cnt[y] + 1; // x 所在的连通分量边数增加
			low[x] = min(low[x], low[y]);
			if (low[y] > dfn[x]) {
				if (cnt[y] % 2 == 0) { // 如果该边是割边,那么要判断 y 所在连通分量边数是否为偶数
					Min = min(Min, edge[i]);
				}
			}
			else { // 如果不是割边,则不需要限制
				Min = min(Min, edge[i]);
			}
		}
		else if(i != (in_edge ^ 1)) { // 如果出现返祖边,即搜索树中,子孙指向祖先的边
            if(dfn[x] > dfn[y]) { // 这条边记录到时间戳更大的点上(只是为了防止重复计数)
                cnt[x] ++;
            }
			low[x] = min(low[x], dfn[y]);
			Min = min(Min, edge[i]);
		}
	}
}
int main() {
	scanf("%d%d", &n, &m);
	tot = 1;
	ll res = 0;
	for (int i = 1; i <= m; i++) {
		int x, y, z;
		scanf("%d%d%d", &x, &y, &z);
		add(x, y, z); add(y, x, z);
		res += z;
	}
    Min = 1e9; // 如果要删边,Min 记录可以删除的边中的最小权值
    tarjan(1, 0);
    if(m & 1) res -= Min;
	cout << res << endl;
	return 0;
}

I Linear Fractional Transformation

根据线性变换保交比性,可以直接推出答案。

#include <bits/stdc++.h>
using namespace std;
typedef long double db;
const db PI = acos(-1.0);
struct Complex {
	db x, y;
	Complex(db _x = 0.0, db _y = 0.0) {
		x = _x;
		y = _y;
	}
	Complex operator - (const Complex& b) const {
		return Complex(x - b.x, y - b.y);
	}
	Complex operator + (const Complex& b) const {
		return Complex(x + b.x, y + b.y);
	}
	Complex operator * (const Complex& b) const {
		return Complex(x * b.x - y * b.y, x * b.y + y * b.x);
	}
	Complex operator / (const Complex& p) const {
		db a = x, b = y, c = p.x, d = p.y;
		return Complex((a * c + b * d) / (c * c + d * d), (b * c - a * d) / (c * c + d * d));
	}
	bool operator == (const Complex& b) const {
		return x == b.x && y == b.y;
	}
	void input() {
		scanf("%Lf%Lf", &x, &y);
	}
	void output() {
		printf("%.10Lf %.10Lf\n", x, y);
	}
}z[5], w[5];
int main() {
	int T;
	cin >> T;
	while (T--) {
		for (int i = 1; i <= 3; i++) {
			z[i].input();
			w[i].input();
		}
		z[4].input();
		bool flag = false;
		for (int i = 1; i <= 3; i++) {
			if (z[i] == z[4]) {
				w[i].output();
				flag = true;
				break;
			}
		}
		if (flag) continue;
		Complex r = ((z[4] - z[1]) * (z[3] - z[2]) * (w[3] - w[1])) / ((z[4] - z[2]) *  (z[3] - z[1]) * (w[3] - w[2]));
		r = (w[1] - r * w[2]) / (Complex(1, 0) - r);
		r.output();
	}
	return 0;
}

L Perfect Matchings

\(2n\) 个点的完全图,从中删除 \(2n-1\) 条边,保证这 \(2n-1\) 条边构成一棵树,求在剩下的图中找到 \(n\) 条无公共点的边的方案数。答案对 \(998244353\) 取模。

\(n\le 2000\)


问题可以转换为在 \(2n\) 个点的树上,匹配 \(n\) 个点对保证每个点对在树上没有边直接相连。

  1. 首先,\(2n\) 个点两两配对,方案数是 \(d_{n}={C_{2n}^nn!\over {2^n}}=\prod_{i=1}^n 2i-1\)

    首先 \(C_{2n}^n\) ,分成两部分,然后第一部分的每个元素依次选择另一部分去配对,方案数是\(n!\)。然后由于每个点对是无序的,共 \(n\) 对,所以要除以 \(2^n\)。最后展开可以得到等式右侧。

  2. 考虑容斥模型,条件 \(P_i\) 表示第 \(i\) 条边不选,那么包含 \(P_i\) 条件的集合为 \(S_i\),而 \(\overline S_i\) 表示不包含第 \(i\) 条边的方案数。假设共 \(m=2n-1\) 条边,那么我们要求的是:

    \[\left|\bigcap_{i=1}^{m}S_i\right|=|U|-\left|\bigcup_{i=1}^m\overline{S_i}\right| \]

    该公式表示 \(m\) 条边都不选择的方案数,就是题目的答案。\(|U|\) 表示 \(2n\) 个点任意配对,每条边都没有限制。后者需要用容斥原理去求。

    每次考虑 \(x\) 个条件 \(P_i\) 不被满足,即指定 \(x\) 条边一定要选,那么它对答案的贡献符号是 \((-1)^x\)

    我们不需要一一枚举每个大小为 \(x\) 的条件集合去求并,然后去求并集大小计算贡献,因为总的集合数量太多,而我们只对集合大小感兴趣。

    所以,可以直接计算当指定 \(x\) 条边一定被选,而其他 \(2n-2x\) 个点任意匹配时的方案数。

    \(f_i\) 表示指定 \(i\) 条边的方案数,然后还需要乘上 \(d_{n-i}\) 才行,表示其他 \(2*(n-i)\) 个点两两任意配对。这组方案数表示了所有大小为 \(i\) 的条件集合并的集合大小之和。

  3. 考虑 \(f_i\) 的求解,需要使用树形DP,具体的,我们需要用状态 \(f_{x,i,0}\) 表示 \(x\) 子树中,选择了 \(i\) 条边并且 \(x\) 没有被选择时的方案数,而 \(f_{x,i,1}\) 相应表示 \(x\) 被选择时的方案数。

    所以就可以遍历 \(x\) 的每个子节点 \(y\),更新:

    • \(f_{x,i,0}*(f_{y,j,0}+f_{y,j,1}) \Rightarrow f_{x,i+j,0}\)

    • \(f_{x,i,1}*(f_{y,j,0}+f_{y,j,1})\Rightarrow f_{x,i+j,1}\)

    • \(f_{x,i,0}*f_{y,j,0} \Rightarrow f_{x,i+j+1,1}\)

    更新思路使用了背包的滚动压缩思想,所以枚举 \(i\)\(j\) 时,需要倒序枚举。这个DP复杂度看起来是 \(O(n^3)\),但因为 \(i\)\(j\) 的遍历范围会根据 \(x\)\(y\) 的子树大小有所限制,所以实际复杂度是 \(O(n^2)\)

    这里复杂度大概可以理解为树上每两个点配对去计算。

    对于 \(x\) 的每个子节点构成的子树,每个\(sz_y\) 需要与 \(sz_x\) 遍历一遍,大概等于 \(sz_x\) 中的每个点与其他点配对的次数。

由于我对容斥的理解有限,所以这里多花了一点篇幅介绍容斥思想。

#include<bits/stdc++.h>
using namespace std;
#define rep(i,j,k) for(int i=int(j);i<=int(k);i++)
#define per(i,j,k) for(int i=int(j);i>=int(k);i--)
typedef long long ll;
const int N = 4005, mod = 998244353;
int n;
vector<int> g[N];
ll f[N][N][2], sz[N], p[N];
void dfs(int x, int fa = 0){
    f[x][0][0] = sz[x] = 1;
    for(auto &y : g[x]) {
        if(y == fa) continue;
        dfs(y, x);
        for(int i = sz[x] / 2; i >= 0; i--) {
            for(int j = sz[y] / 2; j >= 0; j--){
                if(j > 0) {
                    (f[x][i + j][0] += f[x][i][0] * (f[y][j][1] + f[y][j][0]) % mod) %= mod;
                    (f[x][i + j][1] += f[x][i][1] * (f[y][j][1] + f[y][j][0]) % mod) %= mod;
                }
                (f[x][i + j + 1][1] += f[x][i][0] * f[y][j][0] % mod) %= mod;
            }
        }
        sz[x] += sz[y];
    }
}
int main(){
    scanf("%d", &n);
    for(int i=1;i<2*n;i++){
        int u, v;
        scanf("%d%d", &u, &v);
        g[u].push_back(v);
        g[v].push_back(u);
    }
    dfs(1);
    ll res = 0;
    p[0] = 1;
    for(int i=1;i<=n;i++) p[i] = p[i-1] * (2 * i - 1) % mod;
    for(int i=0;i<=n;i++){
        if(i & 1) (res -= (f[1][i][0] + f[1][i][1]) * p[n - i] % mod) %= mod;
        else (res += (f[1][i][0] + f[1][i][1]) * p[n - i] % mod) %= mod;
    }
    printf("%lld\n", (res + mod) % mod);
}

M String Problem

对于字符串的每个前缀,输出其字典序最大的子串。只需要输出左右端点即可。

长度 \(10^6\)


第一种做法是使用后缀自动机。第二种是 KMP 自动机。

具体后面补,贴一个KMP代码

#include <bits/stdc++.h>
using namespace std;
const int N = 1000010;
int nxt[N];
char s[N];
int main(){
    scanf("%s", s+1);
    int n = strlen(s + 1);
    int head = 1, len = 1;
    printf("%d %d\n", 1, 1);
    for(int i=2;i<=n;i++){
        if(s[i] > s[head]) {
            head = i, len = 1;
            printf("%d %d\n", head, head);
            continue;
        }
        while(nxt[len] != 0 && s[head + nxt[len]] < s[i]) {
            head = head + len - nxt[len];
            len = nxt[len];
        }
        if(s[head + nxt[len]] == s[i]) {
            nxt[len + 1] = nxt[len] + 1;
            len ++;
        } else if(s[head + nxt[len]] > s[i]) {
            nxt[++len] = 0;
        }
        printf("%d %d\n", head, head + len - 1);
    }
    return 0;
}
posted @ 2021-12-07 17:20  kpole  阅读(1200)  评论(0编辑  收藏  举报