比赛链接
Problem A. Recommendations
考虑从最小的出现冲突的
开始,进行如下贪心:
保留
最大的
,将其余
增加
。
不难证明这个贪心的正确性。
因此,将所有元素按照 排序,用大根堆模拟这个过程即可。
时间复杂度 。
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 3e5 + 5;
typedef long long ll;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); }
template <typename T> void read(T &x) {
x = 0; int f = 1;
char c = getchar();
for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
x *= f;
}
int n; ll ans, sum;
pair <int, int> a[MAXN];
priority_queue <int> Heap;
int main() {
read(n);
for (int i = 1; i <= n; i++)
read(a[i].first);
for (int i = 1; i <= n; i++)
read(a[i].second);
sort(a + 1, a + n + 1);
int cur = 0;
for (int i = 1; i <= n; i++) {
while (cur < a[i].first && Heap.size()) {
ans += sum - Heap.top();
sum -= Heap.top();
Heap.pop();
cur++;
}
cur = a[i].first;
sum += a[i].second;
Heap.push(a[i].second);
}
while (Heap.size()) {
ans += sum - Heap.top();
sum -= Heap.top();
Heap.pop();
}
cout << ans << endl;
return 0;
}
Problem B. Double Elimination
首先特判掉 的情况,此时答案显然为 。
我们希望关键的队伍存活尽可能长的时间,因此决赛中一定出现了关键队伍。
可以发现,除去第一轮比赛和决赛,剩余的比赛构成了两个相同的树形结构,其中胜者组的树形结构中,一个点代表一场比赛,败者组的树形结构中,一个点代表两场比赛。
在败者组的树形结构中,我们希望关键的队伍始终赢下比赛,因此若一场比赛中不存在关键队伍,其子树内也没有存在关键队伍的比赛。
考虑一支在第一轮中获胜的队伍,此时这支队伍将第一次进入败者组,若其对阵的是一支关键队伍,则它不会对答案产生贡献,否则,则说明这场比赛的子树内没有存在关键队伍的比赛,让这支队伍在第一轮输掉比赛是对答案贡献更大的选择。
因此,我们可以认为,第一场比赛获胜的队伍不会对败者组的比赛产生贡献。
由此,我们便可以设计动态规划解决问题了。
我们按照 DFS 序进行决策,在状态中记录当前考虑到的叶子节点,以及两棵树中上一个考虑到的点与当前点 LCA 的深度,预处理相邻叶子的 LCA 深度 ,可以做到 转移。
时间复杂度 。
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 18;
const int MAXS = (1 << 17) + 5;
typedef long long ll;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); }
template <typename T> void read(T &x) {
x = 0; int f = 1;
char c = getchar();
for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
x *= f;
}
int n, m, nxt[MAXS];
bool f[MAXS];
void getnxt(int l, int r, int depth) {
int mid = (l + r) / 2;
nxt[mid] = depth;
if (l != r) {
getnxt(l, mid - 1, depth + 1);
getnxt(mid + 1, r, depth + 1);
}
}
int main() {
read(n), read(m);
if (m == 0) {
puts("0");
return 0;
}
int goal = 1 << n;
for (int i = 1; i <= m; i++) {
int x; read(x);
f[x] = true;
}
if (n >= 3) getnxt(1, (1 << (n - 2)) - 1, 1);
int ans = 1;
for (int i = 1; i <= goal; i += 2)
ans += f[i] || f[i + 1];
static int dp[MAXN][MAXN], tmp[MAXN][MAXN];
memset(dp, 0, sizeof(dp));
for (int p = 1, q = 1; p <= goal; p += 4, q++) {
memset(tmp, 0, sizeof(tmp));
int cnt = f[p] + f[p + 1] + f[p + 2] + f[p + 3];
for (int i = 0; i <= n - 1; i++)
for (int j = 0; j <= n - 1; j++) {
if (cnt >= 2) chkmax(tmp[nxt[q]][nxt[q]], dp[i][j] + (n - 1 - i) + 2 * (n - 1 - j));
else if (cnt == 1) {
chkmax(tmp[nxt[q]][min(j, nxt[q])], dp[i][j] + (n - 1 - i));
chkmax(tmp[min(i, nxt[q])][nxt[q]], dp[i][j] + 2 * (n - 1 - j));
} else chkmax(tmp[min(i, nxt[q])][min(j, nxt[q])], dp[i][j]);
}
memcpy(dp, tmp, sizeof(tmp));
}
cout << ans + dp[0][0] << endl;
return 0;
}
Problem C. Au Pont Rouge
显然,答案只有 种,可以借助后缀数组对所有子串进行排序,然后二分答案。
二分答案
后,我们需要计算在分出的每一段字典序都不小于
的划分方案数。
注意到在一个字符串后方加字符只会使字典序增大,从一个位置出发划分出一个合法的串的右端点是一个后缀。预处理这个后缀的位置,用前缀和优化 DP 即可。
时间复杂度 。
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
const int MAXM = 1e6 + 5;
const long long INF = 2e18;
typedef long long ll;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); }
template <typename T> void read(T &x) {
x = 0; int f = 1;
char c = getchar();
for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
x *= f;
}
namespace SuffixArray {
const int MAXN = 100005;
const int MAXLOG = 20;
const int MAXC = 256;
int sa[MAXN], rnk[MAXN], height[MAXN];
int Min[MAXN][MAXLOG], bit[MAXN], N;
void init(char *a, int n) {
N = n, a[n + 1] = 0;
for (int i = 0; i <= n + 1; i++)
sa[i] = rnk[i] = 0;
static int x[MAXN], y[MAXN], cnt[MAXN], rk[MAXN];
memset(cnt, 0, sizeof(cnt));
for (int i = 1; i <= n; i++)
cnt[(int) a[i]]++;
for (int i = 1; i <= MAXC; i++)
cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; i--)
sa[cnt[(int) a[i]]--] = i;
rnk[sa[1]] = 1;
for (int i = 2; i <= n; i++)
rnk[sa[i]] = rnk[sa[i - 1]] + (a[sa[i]] != a[sa[i - 1]]);
for (int k = 1; rnk[sa[n]] != n; k <<= 1) {
for (int i = 1; i <= n; i++) {
x[i] = rnk[i];
y[i] = (i + k <= n) ? rnk[i + k] : 0;
}
memset(cnt, 0, sizeof(cnt));
for (int i = 1; i <= n; i++)
cnt[y[i]]++;
for (int i = 1; i <= n; i++)
cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; i--)
rk[cnt[y[i]]--] = i;
memset(cnt, 0, sizeof(cnt));
for (int i = 1; i <= n; i++)
cnt[x[i]]++;
for (int i = 1; i <= n; i++)
cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; i--)
sa[cnt[x[rk[i]]]--] = rk[i];
rnk[sa[1]] = 1;
for (int i = 2; i <= n; i++)
rnk[sa[i]] = rnk[sa[i - 1]] + (x[sa[i]] != x[sa[i - 1]] || y[sa[i]] != y[sa[i - 1]]);
}
int now = 0;
for (int i = 1; i <= n; i++) {
if (now) now--;
while (a[i + now] == a[sa[rnk[i] + 1] + now]) now++;
height[rnk[i]] = now;
}
for (int i = 1; i <= n; i++)
Min[i][0] = height[i];
for (int p = 1; p < MAXLOG; p++) {
int tmp = 1 << (p - 1);
for (int i = 1, j = tmp + 1; j <= n; i++, j++)
Min[i][p] = min(Min[i][p - 1], Min[i + tmp][p - 1]);
}
for (int i = 1; i <= n; i++) {
bit[i] = bit[i - 1];
if (i >= 1 << (bit[i] + 1)) bit[i]++;
}
}
int lcp(int x, int y) {
if (x == y) return N - x + 1;
x = rnk[x], y = rnk[y];
if (x > y) swap(x, y);
int tmp = bit[y - x];
return min(Min[x][tmp], Min[y - (1 << tmp)][tmp]);
}
}
int n, m, tot; ll k;
pair <int, int> a[MAXM];
char s[MAXN];
bool cmp(pair <int, int> a, pair <int, int> b) {
int tmp = SuffixArray :: lcp(a.first, b.first);
chkmin(tmp, a.second - a.first + 1);
chkmin(tmp, b.second - b.first + 1);
if (tmp == a.second - a.first + 1 && tmp == b.second - b.first + 1) return false;
if (tmp == a.second - a.first + 1) return false;
if (tmp == b.second - b.first + 1) return true;
return s[a.first + tmp] > s[b.first + tmp];
}
int nxt[MAXN]; ll dp[MAXN][MAXN];
ll calc(pair <int, int> a) {
for (int i = 1; i <= n; i++) {
nxt[i] = i + 1;
while (nxt[i] <= n + 1 && cmp(a, make_pair(i, nxt[i] - 1))) nxt[i]++;
}
memset(dp, 0, sizeof(dp));
dp[0][1] = 1, dp[0][2] = -1;
for (int i = 0; i <= m - 1; i++) {
for (int j = 1; j <= n + 1; j++) {
dp[i][j] += dp[i][j - 1];
chkmin(dp[i][j], INF);
}
for (int j = 1; j <= n; j++) {
dp[i + 1][nxt[j]] += dp[i][j];
chkmin(dp[i + 1][nxt[j]], INF);
}
}
for (int i = 1; i <= n + 1; i++) {
dp[m][i] += dp[m][i - 1];
chkmin(dp[m][i], INF);
}
return dp[m][n + 1];
}
int main() {
read(n), read(m), read(k);
scanf("%s", s + 1);
SuffixArray :: init(s, n);
for (int i = 1; i <= n; i++)
for (int j = i; j <= n; j++)
a[++tot] = make_pair(i, j);
sort(a + 1, a + tot + 1, cmp);
int l = 1, r = tot;
while (l < r) {
int mid = (l + r) / 2;
if (calc(a[mid]) >= k) r = mid;
else l = mid + 1;
}
for (int i = a[l].first; i <= a[l].second; i++)
putchar(s[i]);
puts("");
return 0;
}
Problem D. Tourism
不存在奇环说明图是二分图。
考虑如下算法:
令每个点以
的概率为二分图左侧的点,
的概率为二分图右侧的点。
在这张二分图上 DP ,找到最小的边数为
的环。
其正确的概率应为
。
将其迭代多次,例如
次,即可保证其出错的概率在
以内。
时间复杂度 ,其中 为迭代次数。
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 85;
const int MAXK = 12;
typedef long long ll;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); }
template <typename T> void read(T &x) {
x = 0; int f = 1;
char c = getchar();
for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
x *= f;
}
int n, k, a[MAXN][MAXN];
bool side[MAXN]; int dp[MAXK][MAXN];
int main() {
srand('X' + 'Y' + 'X');
read(n), read(k);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
read(a[i][j]);
int ans = INT_MAX;
while (clock() <= 2.0 * CLOCKS_PER_SEC) {
for (int i = 0; i <= k; i++)
for (int j = 1; j <= n; j++)
dp[i][j] = INT_MAX;
dp[0][1] = 0;
for (int i = 1; i <= n; i++)
side[i] = rand() % 2 == 0;
for (int i = 1; i <= k; i++)
for (int j = 1; j <= n; j++) {
int tmp = dp[i - 1][j];
if (tmp < INT_MAX) {
for (int l = 1; l <= n; l++)
if (side[l] ^ side[j]) chkmin(dp[i][l], dp[i - 1][j] + a[j][l]);
}
}
chkmin(ans, dp[k][1]);
}
cout << ans << endl;
return 0;
}
Problem E. Strange Function
首先考虑如何将最终数组展开成为最短的原始数组。
则应当将最终数组倒序排列,生成
个
作为展开一层的结果。
由此:
时,倒序的最终数组合法,当且仅当
。
时,倒序的最终数组合法,当且仅当
。
事实上,当 时,合法的情况是很少的,可以直接用搜索解决,只需要优化一下 时的展开方式,例如,可以展开一层,用以上判断标准来判断。
对于
的情况,令
当
时,
对求和符号的贡献为
。
当
时,
对求和符号的贡献为
。
由此,进行简单 DP 即可对 计数。
时间复杂度 。
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 2025;
const int P = 998244353;
typedef long long ll;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); }
template <typename T> void read(T &x) {
x = 0; int f = 1;
char c = getchar();
for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
x *= f;
}
int ans, n, k;
vector <int> a;
bool ok(vector <int> a) {
sort(a.begin(), a.end());
if (k == 3) {
int sum = 0;
for (auto x : a) sum += x;
if (sum > n) return false;
vector <int> b; int tmp = 0;
reverse(a.begin(), a.end());
for (auto x : a) {
tmp++;
while (x--) b.push_back(tmp);
}
a = b; tmp = sum = 0;
reverse(a.begin(), a.end());
for (auto x : a) {
tmp++;
sum += tmp * x;
}
return sum <= n;
}
for (int i = 1; i <= k; i++) {
int sum = 0;
for (auto x : a) sum += x;
if (sum > n) return false;
vector <int> b; int tmp = 0;
reverse(a.begin(), a.end());
for (auto x : a) {
tmp++;
while (x--) b.push_back(tmp);
}
a = b;
}
return true;
}
void work(int pos, int now) {
a.push_back(now);
if (ok(a)) {
ans++;
work(pos + 1, now);
}
a.pop_back();
if (now != 1) work(pos, now - 1);
}
int dp[MAXN][MAXN];
void update(int &x, int y) {
x += y;
if (x >= P) x -= P;
}
int main() {
read(n), read(k);
if (k == 1) {
for (int i = 1; i <= n; i++)
dp[i][n - i] = 1;
for (int i = n; i >= 1; i--)
for (int j = 0; j <= n; j++) {
int tmp = dp[i][j];
if (tmp != 0) {
for (int k = 0; i * k <= j; k++)
update(dp[i - 1][j - i * k], tmp);
}
}
int ans = 0;
for (int i = 0; i <= n; i++)
update(ans, dp[0][i]);
cout << ans << endl;
} else if (k == 2) {
for (int i = 1; i * (i + 1) / 2 <= n; i++)
dp[i][n - i * (i + 1) / 2] = 1;
for (int i = n; i >= 1; i--)
for (int j = 0; j <= n; j++) {
int tmp = dp[i][j];
if (tmp != 0) {
for (int k = 0; i * (i + 1) / 2 * k <= j; k++)
update(dp[i - 1][j - i * (i + 1) / 2 * k], tmp);
}
}
int ans = 0;
for (int i = 0; i <= n; i++)
update(ans, dp[0][i]);
cout << ans << endl;
} else {
work(1, n);
cout << ans << endl;
}
return 0;
}
Problem F. Bad Cryptography
Maybe it's better to put the problem like "You are given a field, implement Pohlig-hellman algorithm on it" to an educational round.