2021ICPC沈阳站
2021ICPC沈阳站
B. Bitwise Exclusive-OR Sequence
题意
对于长度为 \(n\) 的序列,给出 \(m\) 个关系,第 \(i\) 个关系形如 \(a_j \bigoplus a_k = w_i\) ,表示第 \(j\) 个元素和第 \(k\) 个元素的异或值为 \(w_i\) ,问满足所有关系的条件下,这个序列的和最小为多少。如果无解,输出 \(-1\) 。
分析
对于关系 \(a_j \bigoplus a_k = w_i\) ,把 \(j\) 点和 \(k\) 点连一条边,边权为 \(w_i\) 。
无解的情况下,明显一定是存在一个环,在环内各边权存在矛盾。对于每一个连通块,当我们确定其中一个点的点权,其他点的点权是确定的。
判断是否无解,我们只需要看边权是否矛盾,可以 \(dfs\) 每个连通块,如果某个点的点权和邻接点的点权异或值不为边权,则无解。
对于有解的情况,枚举每个连通块的每一个位,对于其中一个点(初始点)可以为 \(0\) 或 \(1\) 。那么在某一位上我们只需要取这个连通块内所有点的 \(min(nums_0, nums_1)\) 作为答案的贡献即可。
Code
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100010, M = 400010;
int n, m;
int h[N], e[M], w[M], ne[M], idx;
int val[N]; // 判断无解时用的点值
bool st[N]; // dfs连通块,判断某个点是否遍历过
int color[N][31]; // color(i, j) 表示第i个点在j位上的值(0 -> 未涂色, 1 -> 1, 2 -> 0)
void add (int a, int b, int c)
{
e[idx] = b; w[idx] = c; ne[idx] = h[a]; h[a] = idx ++ ;
}
// 判断是否无解
void dfs (int u, int v)
{
val[u] = v; st[u] = true;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (st[j]) continue;
if (val[j] == -1) dfs(j, w[i] ^ val[u]);
}
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if ((val[u] ^ val[j]) != w[i]) // 矛盾
{
cout << -1 << endl;
exit(0);
}
}
}
// 找出某一位0和1的数量
void dfs (int u, int & one, int & zero, int c, int bit)
{
color[u][bit] = c; st[u] = true;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!color[j][bit])
{
if (w[i] >> bit & 1)
{
if (c == 1) ++ zero; else ++ one;
dfs(j, one, zero, 3 - c, bit);
}
else
{
if (c == 1) ++ one; else ++ zero;
dfs(j, one, zero, c, bit);
}
}
}
}
int main ()
{
cout.tie(0)->sync_with_stdio(0);
cin >> n >> m;
memset(h, -1, sizeof h);
for (int i = 0, u, v, w; i < m && cin >> u >> v >> w; i++ )
add(u, v, w), add(v, u, w);
memset(val, -1, sizeof val);
for (int i = 1; i <= n; i ++ ) if (!st[i]) dfs(i, 1);
long long res = 0;
memset(st, 0, sizeof st);
for (int i = 1; i <= n; i ++ )
if (!st[i])
{
for (int b = 30; b >= 0; b -- )
{
int one = 1, zero = 0;
dfs(i, one, zero, 1, b);
res = res + min(one, zero) * (1ll << b);
}
}
cout << res << endl;
return 0;
}
F. Encoded Strings I
题意
给定一个长度为 \(n\) 的字符串 \(s\) 。对于每个前缀字符串 \(pre_s\) ,将其重新编码,规则为:
设某个字符 \(c\) 在 \(pre_s\) 中最后一个位置为 \(k\) ,\(chr\) 为 \([k, len(pre_s)]\) 中不同字符的数量。那么把 \(c\) 映射为第 \(chr+1\) 个阿拉伯字母。
求出所有前缀字符串 \(pre_s\) 最大的映射字符串。
分析
\(cnt\) 存储每个字符最后一位后面的不同字符。
从前往后遍历前缀,当加入一个字符时,这个字符最后一个位置变为末尾,\(cnt\) 清空。对于其他在前面出现的字符,它的 \(cnt\) 数组加上这个字符即可。
Code
#include <iostream>
#include <string>
#include <set>
#include <map>
using namespace std;
int main ()
{
int n; cin >> n;
string s, ret; cin >> s;
map<char, set<char> > cnt;
set<char> appear;
for (int i = 0; i < s.size(); i ++ )
{
string cur;
cnt[s[i]].clear();
appear.insert(s[i]);
for (auto c : appear) if (s[i] != c) cnt[c].insert(s[i]);
for (int j = 0; j <= i; j ++ )
cur += char('a' + cnt[s[j]].size());
ret = max(ret, cur);
}
cout << ret << endl;
return 0;
}
J. Luggage Lock
题意
有四位的密码锁,每次可以选择把一段连续的区间向上或者向下转动一格,问把初始状态转成目标状态至少需要多少次旋转。
分析
对于某一位数字 \(a_i\) ,如果要把他向上转动 \(k\) 次,我们同样可以让他向下转动 \(10 - k\) 次,每个数字都有两种状态(除了 \(k=0\) ,也就是初始和目标相同,它可以向上转动 \(10\) 次,也可以向下转动 \(10\) 次,也可以选择不转动)。所以我们可以三进制枚举每一个旋转状态 \(b_1b_2b_3b_4\) 。
当我们得到一个旋转状态后呢?我们需要把这个状态变为全 \(0\) ,由于每次可以选择一段连续区间 \(+1 \ \ or \ \ -1\) ,可以想到 差分数组 ,我们把状态变为全 \(0\) 也就是意味着把差分数组变为全 \(0\) 。
在一个差分数组中,变为全 \(0\) 的最小代价为 \(max(posi, |neg|)\) ,\(posi\) 表示正数总和,\(neg\) 表示负数总和。
Code
#include <iostream>
#include <string>
#include <cmath>
using namespace std;
int rot[5], back[5];
int main ()
{
int T; cin >> T; while( T -- )
{
string a, b; cin >> a >> b;
for (int i = 0; i < 4; i ++ )
rot[i] = a[i] - b[i]; // 向上为正,向下为负
// 枚举每个数字的转动,向上或者向下(0有三种,所以要三进制枚举)
int m = 3 * 3 * 3 * 3, ret = 1e9;
for (int i = 0; i < m; i ++ )
{
int state = i;
for (int j = 0; j < 4; j ++ )
{
if (state % 3 == 1)
{
// 为1,反方向旋转
if (rot[j] > 0) back[j] = rot[j] - 10;
else back[j] = 10 + rot[j];
}
else if (state % 3 == 2 && !rot[j])
{
// 为2,特判rot为0,由于在模1的时候0为10,所以这里为-10
back[j] = -10;
}
else back[j] = rot[j];
state /= 3;
}
// 把旋转数组变为全0,即差分数组变为0
int posi = 0, neg = 0; // 差分数组变为0的次数:max(posi, neg)
for (int j = 0; j < 4; j ++ )
{
int dif = (j ? back[j] - back[j-1] : back[j]);
if (dif > 0) posi += dif;
else neg += dif;
}
ret = min(ret, max(posi, abs(neg)));
}
cout << ret << endl;
}
return 0;
}
J. Perfect Matchings
题意
对于一个 \(2 * n\) 个顶点的完全图,删除给定的一颗生成树,求剩下图的完美匹配数量有多少。
完美匹配,指最大数量的边集合,集合内任意两条边都没有公共顶点。
分析
-
对于一个 \(2 * n\) 顶点的完全图,它的完美匹配数量有 \(\prod_{i=1}^{n}2*i - 1\) 个。
把 \(n\) 个顶点放到左边去匹配右边 \(n\) 个,选择 \(n\) 个顶点 \(C_{2n}^{n}\) ,匹配为 \(n!\) 。
然后对于每一个匹配边,它的一个结点在右边时贡献一次,左边时又贡献一次,所以要除以二。
所以完美匹配数量为 \(\dfrac{C_{2n}^{n} \times n!}{2}\) 个。
把 \(C_{2n}^{n}\) 中的偶数项除以二后与 \(n!\) 抵消,所以匹配数量为 \(1 \times 3 \times 5 \ldots (2 \times n - 1) = \prod_{i=1}^{n}2*i - 1\) 。
-
容斥原理
用总的图的所有匹配数量减去其中有一些匹配边在生成树上的数量,枚举有几条边在生成树,减去所有不合法情况就是合法的情况。
-
计数dp,枚举在生成树上的不合法情况
设 \(f(i, j, 0/1)\) 表示以 \(i\) 为子树,匹配数量为 \(j\) ,\(i\) 参与/不参与匹配时的方案数量。
树上背包问题,设 \(j\) 为 \(i\) 的儿子,转移方程为:
\[f(i, x + y, 0) += f(i, x, 0) * (f(j, y, 0) + f(j, y, 1)), y > 0 \\ f(i, x + y, 1) += f(i, x, 1) * (f(j, y, 0) + f(j, y, 1)), y > 0 \\ f(i, x + y + 1, 1) += f(i, x, 0) * f(j, y, 0) \]可以枚举每个 \(i\) 子树的大小使复杂度达到 \(O(n^2)\) 。
Code
#include <iostream>
#include <cstring>
using namespace std;
const int N = 4010, M = 8010, mod = 998244353;
int n, m;
int h[N], e[M], ne[M], idx;
int siz[N]; // 每个子树的结点个数
int f[N][N][2]; // f(i, j, k) 表示以i为根的树,取j个配对,i有无选中的方案数量
int p[N]; // p(i) 表示i个结点组成的完全图可配对的数量
void add (int a, int b)
{
e[idx] = b; ne[idx] = h[a]; h[a] = idx ++ ;
}
void dfs (int u, int fa)
{
f[u][0][0] = 1; siz[u] = 1;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j == fa) continue;
dfs(j, u);
for (int x = siz[u] / 2; x >= 0; x -- )
for (int y = siz[j] / 2; y >= 0; y -- )
{
if (y > 0)
{
f[u][x + y][0] = (f[u][x + y][0] + 1ll * f[u][x][0] * (f[j][y][0] + f[j][y][1]) % mod) % mod;
f[u][x + y][1] = (f[u][x + y][1] + 1ll * f[u][x][1] * (f[j][y][0] + f[j][y][1]) % mod) % mod;
}
f[u][x + y + 1][1] = (f[u][x + y + 1][1] + 1ll * f[u][x][0] * f[j][y][0] % mod) % mod;
}
siz[u] += siz[j];
}
}
signed main ()
{
int n; cin >> n;
p[0] = 1; for (int i = 1; i <= n; i ++ ) p[i] = 1ll * p[i-1] * (2 * i - 1) % mod;
memset(h, -1, sizeof h);
for (int i = 1, u, v; i < 2 * n && cin >> u >> v; i ++ ) add(u, v), add(v, u);
dfs(1, -1);
int res = 0;
for (int i = 0; i <= n; i ++ ) // 枚举生成树里的配对数量
{
if (i & 1) res = (1ll * res - 1ll * (f[1][i][0] + f[1][i][1]) * p[n - i] % mod) % mod;
else res = (1ll * res + 1ll * (f[1][i][0] + f[1][i][1]) * p[n-i] % mod) % mod;
}
cout << (res % mod + mod) % mod << endl;
return 0;
}