2022-2023 ACM-ICPC Nordic Collegiate Programming Contest (NCPC 2022)
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
\(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;
}