2022-2023 ACM-ICPC Nordic Collegiate Programming Contest (NCPC 2022)

发布时间 2023-09-20 01:42:50作者: Zeoy_kkk

F. Foreign Football

一共有\(n\)支队伍,每支队伍的名称为\(s_i\),给定一个\(n \times n\)的矩阵,\(a_{i,j}\)代表第\(i\)支队伍和第\(j\)支队伍名字的拼接,即\(a_{i,j}=s_i+s_j\),请你通过该矩阵推断出所有队伍的名称,如果有多个解,输出MANY,如果有唯一解,输出UNIQUE,并输出所有队伍的名称,如果无解,输出NONE

注意:队伍名称非空

\(2 \leq n \leq 500,\sum a_{i,j} \leq 1e6\)

题解:高斯消元 + 字符串哈希

  • 如果\(n \geq 3\),显然我们可以对\(s_1+s_2,s_2+s_3,s_1+s_3\)这三个方程进行高斯消元,如果有唯一解我们就可以得到\(s_1\)的长度,那么我们就能够通过\(s_1\)来确定所有队伍的名称,最后枚举一遍矩阵进行检查,如果检查不通过,输出NONE
  • 对于\(n = 2\)的情况,我们考虑特判,我们不妨枚举\(s_1\)的长度后得到\(s_1,s_2\),通过字符串哈希\(O(1)\)检查\(a_{1,2}\)\(a_{2,1}\)能否对应即可,如果能够多次对应,那么输出MANY,其他情况对应输出即可
int n;
string s[N][N], ans[N];
double a[10][10];
int h1[M], h2[M], p[M], base = 131;
int get_hash1(int l, int r) { return ((h1[r] - h1[l - 1] * p[r - l + 1] % mod) % mod + mod) % mod; }
int get_hash2(int l, int r) { return ((h2[r] - h2[l - 1] * p[r - l + 1] % mod) % mod + mod) % mod; }

int Gauss(int n, int m) // n行m列的增广矩阵
{
    int r = 0;                       // 增广矩阵的秩
    for (int j = 1; j <= m - 1; ++j) // 枚举系数矩阵的列
    {
        int mx = r + 1;               // 当前所在行号
        for (int i = mx; i <= n; ++i) // 枚举行来找到该列绝对值最大非0系数
        {
            if (fabs(a[i][j]) > fabs(a[mx][j]))
                mx = i;
        }

        if (fabs(a[mx][j]) < eps) // 对角线上主元素为0,没有唯一解,矩阵的秩不用加1
            continue;

        for (int i = 1; i <= m; ++i) // 将这最大系数这一行和对角线那一行进行交换
            swap(a[r + 1][i], a[mx][i]);

        for (int i = m; i >= j; --i) // 将这一行的主元素系数变为1
            a[r + 1][i] /= a[r + 1][j];

        for (int i = 1; i <= n; ++i) // 消去主元所在列的其他行的主元
        {
            if (i != r + 1)
            {
                double t = a[i][j] / a[r + 1][j];
                for (int k = 1; k <= m; ++k)
                    a[i][k] -= t * a[r + 1][k];
            }
        }
        r++;
    }

    if (r < m - 1) // 矩阵的秩小于未知量的个数(m - 1)
    {
        for (int i = r + 1; i <= n; ++i)
        {
            if (fabs(a[i][m]) > eps)
                return 0; // 无解(增广矩阵和系数矩阵的秩不同)
        }
        return 2; // 有无穷多组解
    }
    return 1;
}

void solve()
{
    cin >> n;
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n; ++j)
            cin >> s[i][j];
    if (n == 2)
    {
        int len1 = s[1][2].length(), len2 = s[2][1].length();
        string tmp = s[1][2];
        if (len1 != len2 || len1 <= 1)
        {
            cout << "NONE" << endl;
            return;
        }
        s[1][2] = " " + s[1][2], s[2][1] = " " + s[2][1];
        p[0] = 1;
        for (int i = 1; i <= len1; ++i)
            h1[i] = (h1[i - 1] * base % mod + s[1][2][i]) % mod;
        for (int i = 1; i <= len1; ++i)
            h2[i] = (h2[i - 1] * base % mod + s[2][1][i]) % mod;
        for (int i = 1; i <= max(len1, len2); ++i)
            p[i] = p[i - 1] * base % mod;
        int cnt = 0, p = 0;
        for (int i = 1; i <= len1 - 1; ++i)
        {
            if (get_hash1(1, i) == get_hash2(len1 - i + 1, len1) && get_hash1(i + 1, len1) == get_hash2(1, len1 - i))
                cnt++, p = i;
        }
        if (cnt == 0)
            cout << "NONE" << endl;
        else if (cnt == 1)
        {
            cout << "UNIQUE" << endl;
            cout << tmp.substr(0, p) << endl;
            cout << tmp.substr(p) << endl;
        }
        else
            cout << "MANY" << endl;
        return;
    }
    a[1][4] = (int)s[1][2].size();
    a[2][4] = (int)s[2][3].size();
    a[3][4] = (int)s[1][3].size();
    a[1][1] = 1, a[1][2] = 1, a[1][3] = 0;
    a[2][1] = 0, a[2][2] = 1, a[2][3] = 1;
    a[3][1] = 1, a[3][2] = 0, a[3][3] = 1;
    int res = Gauss(3, 4);
    if (!res)
    {
        cout << "NONE" << endl;
        return;
    }
    else if (res == 2)
    {
        cout << "MANY" << endl;
        return;
    }
    else
    {
        for (int i = 1; i <= 3; ++i)
        {
            if (a[i][4] <= 0)
            {
                cout << "NONE" << endl;
                return;
            }
        }
        ans[1] = s[1][2].substr(0, (int)a[1][4]);
        for (int i = 2; i <= n; ++i)
            ans[i] = s[1][i].substr((int)ans[1].size());
        for (int i = 1; i <= n; ++i)
        {
            for (int j = 1; j <= n; ++j)
            {
                if (i == j)
                    continue;
                if ((ans[i] + ans[j]) != s[i][j])
                {
                    cout << "NONE" << endl;
                    return;
                }
            }
        }
    }
    cout << "UNIQUE" << endl;
    for (int i = 1; i <= n; ++i)
        cout << ans[i] << endl;
}

G. Graduation Guarantee

image-20230920010642667

\(1 \leq k \leq n \leq 5000\)

题解:概率\(DP\)

  • 显然我们贪心的考虑一定先回答正确率高的题目,所以考虑根据题目的正确率降序排列

  • 我们设计状态:\(dp[i][j]\)代表只从前\(i\)个题目中选择题目回答,得到\(j\)分的概率

  • 考虑转移:

\(dp[0][0] = 1\)

\(dp[i][j] = dp[i-1][j-1] * p[i] + dp[i-1][j+1]*(1 - p[i])\)

注意:得分可能是负的,所以我们枚举第二维的时候需要从\(-n\)开始,加个偏移量即可

  • 最后答案就是对所有得分超过\(k\)的状态取个\(max\)即可
int n, k;
double dp[N][N << 1], p[N];

void solve()
{
    cin >> n >> k;
    for (int i = 1; i <= n; ++i)
        cin >> p[i];
    sort(p + 1, p + n + 1, greater<double>());
    dp[0][N] = 1;
    for (int i = 1; i <= n; ++i)
        for (int j = N - n; j <= n + N; ++j)
            dp[i][j] = dp[i - 1][j - 1] * p[i] + dp[i - 1][j + 1] * (1 - p[i]);
    double ans = 0;
    for (int i = 1; i <= n; ++i)
    {
        double res = 0;
        for (int j = k + N; j <= n + N; ++j)
            res += dp[i][j];
        ans = max(ans, res);
    }
    cout << fixed << setprecision(6) << ans << endl;
}

K. Keyboard Queries

给定一个长度为\(n\),但是未知的字符串\(s\),给定\(q\)次询问:

  • \(1 , l, r\):告诉你\([l,r]\)的子串是回文串
  • \(2,a,b,x,y\):询问\([a,b]\)\([x,y]\)区间的子串是否一样,存在\(3\)种回答:一样,不一样,不知道

题解:启发式合并并查集 + 线段树维护字符串哈希

  • 显然需要线段树维护正串哈希和反串哈希,首先\(build\)的时候保证每个叶子节点的\(hash\)值不一样
  • 对于第一个操作,我们可以知道哪些位置的字符是一样的,所以我们考虑通过并查集连边,但是如果我们暴力连边的话会超时,所以我们考虑只对还没有确定位置连边,那么我们最多只会连\(n-1\)条边
  • 所以我们现在的问题就是对于一个询问,我们需要快速找到\([l,r]\)中第一个没有确定的位置进行连边(不确定的位置代表该位置对称的两侧的字符不相同),我们考虑通过二分来实现,因为我们通过线段树维护了哈希,所以我们可以二分出第第一个不相等的位置,例如\(abcdecba\)中第一个不相等的位置为\(4\)\(check\)时我们只要比较前缀的正串哈希和后缀的反串哈希是否相同即可,复杂度\(O(nlog^2n)\)
  • 我们考虑连边的时候利用并查集启发式合并即可,即将\(sz\)小的合并给\(sz\)大的,那么合并的同时在线段树上单点修改\(hash\)即可
  • 对于询问\(2\),一个显然的回答,如果长度不一样,那么一定不相同,如果在线段树上查询出相同,那么一定相同,否则就是不知道
  • 复杂度:\(O(nlog^2n + qlogn)\)
#include <bits/stdc++.h>
using namespace std;
#define Zeoy std::ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0)
#define all(x) (x).begin(), (x).end()
#define rson id << 1 | 1
#define lson id << 1
#define int long long
#define mpk make_pair
#define endl '\n'
typedef unsigned long long ULL;
typedef long long ll;
const int inf = 0x3f3f3f3f;
const ll INF = 0x3f3f3f3f3f3f3f3f;
const int mod = 1e9 + 7;
const double eps = 1e-9;
const int N = 1e5 + 10, M = 4e5 + 10;

int n, q, fa[N], sz[N], pw[N], base = 131;
vector<int> vec[N];

struct info
{
    int hash, rhash, len;
    info(int hash = 0, int rhash = 0, int len = 0) : hash(hash), rhash(rhash), len(len) {}
    friend info operator+(const info &a, const info &b)
    {
        info c;
        c.len = a.len + b.len;
        c.hash = (a.hash * pw[b.len] % mod + b.hash) % mod;
        c.rhash = (b.rhash * pw[a.len] % mod + a.rhash) % mod;
        return c;
    }
};
struct SEG
{
    info val;
} seg[N << 2];
void up(int id) { seg[id].val = seg[lson].val + seg[rson].val; }
void build(int id, int l, int r)
{
    if (l == r)
    {
        seg[id].val = info(l, l, 1);
        return;
    }
    int mid = l + r >> 1;
    build(lson, l, mid);
    build(rson, mid + 1, r);
    up(id);
}
void change(int id, int l, int r, int x, int v)
{
    if (l == r)
    {
        seg[id].val.hash = seg[id].val.rhash = v;
        return;
    }
    int mid = l + r >> 1;
    if (x <= mid)
        change(lson, l, mid, x, v);
    else
        change(rson, mid + 1, r, x, v);
    up(id);
}
info query(int id, int l, int r, int ql, int qr)
{
    if (ql <= l && r <= qr)
        return seg[id].val;
    int mid = l + r >> 1;
    if (qr <= mid)
        return query(lson, l, mid, ql, qr);
    else if (ql > mid)
        return query(rson, mid + 1, r, ql, qr);
    else
        return query(lson, l, mid, ql, qr) + query(rson, mid + 1, r, ql, qr);
}

void merge(int x, int y)
{
    int fx = fa[x], fy = fa[y];
    if (fx != fy)
    {
        if (sz[fx] > sz[fy])
            swap(fx, fy);
        for (auto u : vec[fx])
        {
            fa[u] = fy;
            change(1, 1, n, u, fy);
            vec[fy].push_back(u);
        }
        sz[fy] += sz[fx];
        fa[fx] = fy;
        vec[fx].clear();
    }
}

void solve()
{
    cin >> n >> q;
    pw[0] = 1ll;
    for (int i = 1; i <= n; ++i)
    {
        fa[i] = i;
        sz[i] = 1ll;
        pw[i] = pw[i - 1] * base % mod;
        vec[i].push_back(i);
    }
    build(1, 1, n);
    while (q--)
    {
        int op, L, R, a, b, x, y;
        cin >> op;
        if (op == 1)
        {
            cin >> L >> R;
            int Mid1 = L + R >> 1, Mid2 = L + R + 1 >> 1;
            auto check = [&](int l1, int r1, int l2, int r2)
            {
                return query(1, 1, n, l1, r1).hash == query(1, 1, n, l2, r2).rhash;
            };
            while (!check(L, Mid1, Mid2, R))
            {
                int l = 1, r = Mid1;
                while (l <= r)
                {
                    int mid = l + r >> 1;
                    if (check(L, L + mid - 1, R - mid + 1, R))
                        l = mid + 1;
                    else
                        r = mid - 1;
                }
                merge(L + r, R - r);
            }
        }
        else
        {
            cin >> a >> b >> x >> y;
            if (b - a + 1 != y - x + 1)
                cout << "Not equal" << endl;
            else if (query(1, 1, n, a, b).hash == query(1, 1, n, x, y).hash)
                cout << "Equal" << endl;
            else
                cout << "Unknown" << endl;
        }
    }
}
signed main(void)
{
    Zeoy;
    int T = 1;
    // cin >> T;
    while (T--)
        solve();
    return 0;
}