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\) 个顶点的完全图,删除给定的一颗生成树,求剩下图的完美匹配数量有多少。

完美匹配,指最大数量的边集合,集合内任意两条边都没有公共顶点。

分析

  1. 对于一个 \(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\)

  2. 容斥原理

    用总的图的所有匹配数量减去其中有一些匹配边在生成树上的数量,枚举有几条边在生成树,减去所有不合法情况就是合法的情况。

  3. 计数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;
}
posted @ 2021-11-21 19:56  Horb7  阅读(586)  评论(0编辑  收藏  举报