NordicOI 2023
A. ChatNOI
题目描述
给定一个由 \(N\) 个小写英文单词组成的文章,我们定义一个 \(k+1\) 个单词的可能性为其在文章中的出现次数。现在给出一个句子的前 \(k\) 个单词,你要补全后面的 \(m\) 个单词,使得其中所有长度为 \(k+1\) 的字串的可能性最小值最大。有 \(Q\) 次询问。
思路
因为我们是不断在后面加入单词,所以我们可以把这个文章变成一个图。每个的字串 \([l,l+k-1]\) 连向 \([l+1,l+k]\),边权为 \([l+1,l+k]\) 的可能性。在这个图上我们要找到一个长度为 \(m\) 的路径使得其边权最小值最大。
这里很显然点,边和边权总和的数量级都是 \(O(N)\) 的,而不同的边权数量是 \(O(\sqrt N)\) 的,因为最优情况下边权依次是 \(1,2,\dots,\sqrt N\)。
这个有什么用呢?我们可以暴力从大到小枚举每种边权 \(x\),并把所有 \(\ge x\) 的边加入。令 \(u\) 为询问给出的句子对应的点,如果此时第一次从 \(u\) 开始可以行走至少 \(m\) 步,那么这个询问的答案为 \(x\),使用拓扑排序解决。
空间复杂度 \(O(N+Q)\),时间复杂度 \(O((N+Q)\sqrt N)\)。
代码
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int MAXN = 500001, MAXQ = 100001, INF = INT_MAX;
const ll b[2] = {29, int(1e9) + 9}, MOD = int(1e9) + 7;
struct query {
int m, id;
ull x;
}a[MAXQ];
int n, k, tot, q, in[MAXN], dp[MAXN], f[MAXN], fid[MAXN];
ull sum[MAXN], Pow[MAXN];
string S[MAXN];
map<ull, int> mp, cnt;
vector<tuple<int, int, int>> e[MAXN];
vector<int> ve, ans[MAXQ];
vector<tuple<int, int, int, int>> vec;
int Hash(const string &s) {
int ret = 0;
for(char c : s) {
ret = (1ll * ret * b[0] % MOD + c - 'a' + 1) % MOD;
}
return ret;
}
ull Calc(int l, int r) {
return sum[r] - sum[l - 1] * Pow[r - l + 1];
}
void topo_sort() {
queue<int> que;
fill(in + 1, in + tot + 1, 0);
for(int i = 1; i <= tot; ++i) {
dp[i] = 0;
for(auto [v, w, id] : e[i]) {
in[v]++;
}
}
for(int i = 1; i <= tot; ++i) {
if(!in[i]) {
que.push(i);
}
}
for(; !que.empty(); ) {
int u = que.front();
que.pop();
for(auto [v, w, id] : e[u]) {
if(dp[u] + 1 > dp[v]) {
dp[v] = dp[u] + 1, f[v] = u, fid[v] = id;
}
if(!(--in[v])) {
que.push(v);
}
}
}
for(int i = 1; i <= tot; ++i) {
if(in[i]) {
dp[i] = INF;
for(auto [v, w, id] : e[i]) {
if(in[v]) {
f[v] = i, fid[v] = id;
}
}
}
}
}
int main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n >> k;
Pow[0] = 1;
for(int i = 1; i <= n; ++i) {
string s;
cin >> s;
S[i] = s;
sum[i] = sum[i - 1] * b[1] + Hash(s);
Pow[i] = Pow[i - 1] * b[1];
}
for(int i = 1; i <= n - k; ++i) {
cnt[Calc(i, i + k)]++;
}
for(int i = 1; i <= n - k + 1; ++i) {
ull h = Calc(i, i + k - 1);
if(!mp.count(h)) {
mp[h] = ++tot;
}
if(i > 1) {
vec.emplace_back(cnt[Calc(i - 1, i + k - 1)], mp[Calc(i - 1, i + k - 2)], mp[h], i + k - 1);
ve.emplace_back(cnt[Calc(i - 1, i + k - 1)]);
}
}
cin >> q;
for(int i = 1; i <= q; ++i) {
cin >> a[i].m;
for(int j = 1; j <= k; ++j) {
string s;
cin >> s;
a[i].x = a[i].x * b[1] + Hash(s);
}
a[i].x = mp[a[i].x];
}
sort(ve.begin(), ve.end()), ve.erase(unique(ve.begin(), ve.end()), ve.end()), reverse(ve.begin(), ve.end());
sort(vec.begin(), vec.end(), greater<tuple<int, int, int, int>>());
int j = 0;
for(int x : ve) {
for(; j < int(vec.size()); ++j) {
auto [w, u, v, id] = vec[j];
if(w >= x) {
e[v].emplace_back(u, w, id);
}else {
break;
}
}
topo_sort();
for(int i = 1; i <= q; ++i) {
if(!a[i].x || ans[i].size() || dp[a[i].x] < a[i].m) {
continue;
}
int u = a[i].x, cnt = 1;
for(; cnt <= a[i].m; u = f[u], cnt++) {
ans[i].emplace_back(fid[u]);
}
}
}
for(int i = 1; i <= q; ++i) {
if(ans[i].empty()) {
for(int j = 1; j <= a[i].m; ++j) {
cout << S[1] << " \n"[j == a[i].m];
}
}else {
for(int x : ans[i]) {
cout << S[x] << " ";
}
cout << "\n";
}
}
return 0;
}
B. Ice Cream Machines
题目描述
有 \(N\) 个人,第 \(i\) 个人想吃第 \(c_i\) 种口味的冰淇淋。有 \(k\) 个冰淇淋机,每种机器都可以制作任意口味的冰淇淋,但是每次让一个冰淇淋机制作和之前不同的冰淇凌都要清洗一次。第一次使用也要清洗。
求至少要清洗几次。
思路
用贪心解决。我们用 set
记录每个冰淇凌机当前是什么冰淇凌,该冰淇凌下次出现的位置,我们肯定会选择下次出现位置更靠后的清洗。
空间复杂度 \(O(N)\),时间复杂度 \(O(N\log N)\)。
代码
#include<bits/stdc++.h>
using namespace std;
using pii = pair<int, int>;
const int MAXN = 200001, MAXM = 200001, MAXK = 200001;
int n, m, k, c[MAXN], cnt[MAXM], a[MAXK], pos[MAXM], ans;
set<pii, greater<pii>> s;
vector<int> ve[MAXM];
int main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n >> m >> k;
for(int i = 1; i <= n; ++i) {
cin >> c[i];
ve[c[i]].emplace_back(i);
}
for(int i = 1; i <= m; ++i) {
ve[i].emplace_back(n + 1);
}
for(int i = 1; i <= k; ++i) {
s.insert({n + 1, i});
}
for(int i = 1; i <= n; ++i) {
if(!pos[c[i]]) {
ans++;
auto [cur, p] = *s.begin();
s.erase(s.begin());
s.insert({*upper_bound(ve[c[i]].begin(), ve[c[i]].end(), i), p});
pos[a[p]] = 0;
pos[c[i]] = p;
a[p] = c[i];
}else {
s.erase({i, pos[c[i]]});
s.insert({*upper_bound(ve[c[i]].begin(), ve[c[i]].end(), i), pos[c[i]]});
}
}
cout << ans;
return 0;
}