2024.10.13 模拟赛
2024.10.13 模拟赛
T1「KDOI-10」商店砍价
赛时直接口胡出一个错误的贪心。一开始的想法是按照 \(v[i]\) 排序,然后假设输入的长度为 \(n\) 位。那么对于排序后 \(n - 5\) 位直接选择操作一。对于剩下的,直接 bdfs 所有情况选答案,复杂度 \(O(n \log n)\)。貌似可行,结果随便一个数据就能 hack。比如数字 \(1\) 的 \(v[i]\) 是 \(9\),数字 \(9\) 的 \(v[i]\) 是 \(1\)。老实了。
那么 \(dp\)?设 \(f[i]\) 表示考虑到第 \(i\) 的答案,维护的信息好像差一点。那么 \(f[i][j]\) 的第二维又可以表示什么?坏了,根本想不出来。
等等,对于大部分情况,显然操作一更优,那么可以先假设全部执行操作一,然后再去反着找有没有比操作一更优的操作二。\(f[i]\) 表示考虑了 \(i\) 位使答案变得更优的值,那么最终的答案就是 \(\sum v[i] - \max\{f[i]\}\)
至于 \(f[i]\) 的转移也很简单,\(f_j = \max^{n}_{i=1}\{f_{j - 1} + v_{a_i} - a_i \times 10^{j - 1}\}\)
转移的时候要倒着扫保证 \(10^k\) 的意义
复杂度 \(O(n^2)\)
考虑继续优化,这个时候我们一开始的贪心就有用了,我们发现在枚举 \(j\) 的时候最多只需要枚举 \(5\) 位,就像一开始的贪心一样,枚举多了没有意义因为操作一一定更优秀,所以复杂度为 \(O(nK)\),\(K<5\)
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
using namespace std;
const int N = 1e5 + 5;
const int M = 2e1 + 1;
int ans;
int a[N], v[N];
char s[N];
int ten[M], f[M];
signed main()
{
int c, T;
cin >> c >> T;
ten[0] = 1;
for (rint i = 1; i <= 20; i++) ten[i] = ten[i - 1] * 10;
while (T--)
{
cin >> (s + 1); ans = 0;
int n = strlen(s + 1);
for (rint i = 1; i <= n; i++) a[i] = s[i] - '0';
for (rint i = 1; i <= 9; i++) cin >> v[i];
for (rint i = 1; i <= n; i++) ans += v[a[i]];
memset(f, 0xcf, sizeof f);
f[0] = 0;
for (rint i = n; i >= 1; i--)
{
int k = n - i + 1;
for (rint j = min(5ll, k); j >= 1; j--)
{
f[j] = max(f[j], f[j - 1] + v[a[i]] - a[i] * ten[j - 1]);
}
}
int dt = 0;
for (rint i = 1; i <= 5; i++) dt = max(dt, f[i]);
cout << ans - dt << endl;
}
return 0;
}
T2「KDOI-10」水杯降温
PS:已经被 hack 了,老实了,被这篇帖子link直接制裁。
题外话,@estimate_error这个人机赛时无限趋近于正解(指通过原数据)然后反手写了一个充分不必要条件杀死自己拿下 16 pts 高分。est:“样例过了。我这个构造没问题!”大样例一测,第一个都对不上。
首先,est 在赛时提出了两个水温可以变成 0 的条件:
-
- 对于结点 \(x\),其所有直接儿子 \(y\) ,\(\sum a_i ≤a_x\)
-
- 对于每一个从下往上走的链是单调不减的
貌似这两个条件是具有充分性的,但是并不具有必要性,由于数据很水,代码可以拿到 16pts
我们将原操作进行以下优化从而使必要性更紧,但不能保证必要性,说不定乱搞就过了呢:
-
- 我们不再开树上前缀和数组树上差分维护 \(\sum a_i\) 而是让 \(a_y\) 的信息往 \(a_x\) 加,\(y ∈ x\),最后累加 \(a[x]\) 得到 \(res\) 与它们的公共父亲比大小
-
- 对于一开始提出的条件一,要注意累加后小于 0 的情况。此时必要性一定不成立。要取个 \(max\)
-
- 不难发现起决定性作用的一定是根节点,因为操作二一定会影响到根节点,所以
a[1] = max(a[1], 0)
,最后a[i]-=a[fa[i]]
,但是这个操作倒着处理保证顺序
- 不难发现起决定性作用的一定是根节点,因为操作二一定会影响到根节点,所以
诶,这样只需要每次执行一次 dfs 就可以,对于赛时数据还是可以轻松通过的。但是被 hack 了。说明该方案是具有错误性的,就比如在根节点下面接一颗完全二叉树并使最后一排全为 0。
(所以是不是在代码里加个check如果最后一排全是零就输出Shuiniao就行了/doge)
当然如果这个题有正解 idea 的欢迎私信教教这个蒟蒻怎么做。。。。
有一篇貌似看起来很对的做法,留个link
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
using namespace std;
const int N = 1e5 + 5;
int c, T;
int n, a[N], fa[N];
vector<int> vec[N];
bool flag;
void dfs(int x)
{
int res = 0;
for (rint y : vec[x])
{
if (vec[x].size() == 1) a[y] = max({0ll, a[x], a[x] + a[y]});
else a[y] = max(0ll, a[x] + a[y]);
res += a[y];
dfs(y);
}
if (res > a[x]) flag = 0;
}
signed main()
{
cin >> c >> T;
while (T--)
{
cin >> n;
flag = 1;
for (rint i = 2; i <= n; i++)
{
int x;
cin >> x;
fa[i] = x;
vec[x].push_back(i);
}
for (rint i = 1; i <= n; i++) cin >> a[i];
for (rint i = n; i >= 1; i--) a[i] -= a[fa[i]];
a[1] = max(a[1], 0ll);
dfs(1);
cout << (flag ? "Huoyu" : "Shuiniao") << endl;
for (rint i = 1; i <= n; i++) vec[i].clear();
}
return 0;
}
update:2024.10.14 21:54
感谢 @roger_yrj 大佬私信赐教
如果把加热操作都撤销,树根会小于等于0,然后每个儿子小于自己的父亲。只要满足上面这种情况,就能通过加热操作把所有数变为 \(0\)。之后,想办法通过树链 \(-1\) 的操作,达到上面的情况。而对于树链 \(-1\) 的实质,就是这条链上的点,把每个不在这条链上的儿子和自己的差全部缩小了 \(1\)。这与目标相违背,所以希望操作越少越好,但又希望将根节点的值变成非正数,所以操作又不能太少,所以我们对于每一颗子树,操作的次数是一个区间,进行树形 dp,处理每一颗子树的区间,区间的下限很好求,就是每个儿子的下限之和与自己权值的最大值。处理出这个点对所有儿子的差,然后二分这个答案。在二分答案的过程中有两个 check,之所以很多代码都会被 hack 掉是因为总的操作次数不能超过儿子的上界和,因为每次操作,都会缩小儿子和父亲的差。这样这个题就做完了。实现方式很多,roger_yrj 的代码是按照边递归边二分的,我将上面的错误代码微调了一下。此时应该是具有正确性的了。
对于二分的上下界是不是也有讲究啊。。。。。我不知道,但是确实已经很难在叉掉这个代码了。就这样吧。
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
using namespace std;
const int N = 1e5 + 5;
const int inf = 2e12;
int c, T, n;
int a[N], fa[N];
int f[N];
vector<int> e[N];
bool check(int k, int x)
{
int sum = 0, sum2 = 0;
for (rint y : e[k])
{
if (x + a[y] > f[y]) return 0;
sum += max(0ll, x + a[y]);
sum2 += f[y];
}
if (sum <= x && x <= sum2) return 1;
return 0;
}
signed main()
{
cin >> c >> T;
while (T--)
{
cin >> n;
for (rint i = 2; i <= n; i++) cin >> fa[i];
for (rint i = 1; i <= n; i++) cin >> a[i];
for (rint i = n; i >= 1; i--) a[i] -= a[fa[i]];
for (rint i = n; i >= 1; i--)
{
if (!e[i].size()) f[i] = inf;
else
{
int l = 0, r = inf;
while (l < r)
{
int mid = (l + r) >> 1;
if (check(i, mid))
{
f[i] = mid;
l = mid + 1;
}
else
{
r = mid;
}
}
}
e[fa[i]].push_back(i);
}
for (rint i = 2; i <= n; i++)
if (a[i] > 0)
f[1] = -inf;
if (a[1] > f[1]) puts("Shuiniao");
else puts("Huoyu");
for (rint i = 1; i <= n; i++) e[i].clear();
memset(f, 0, sizeof f);
}
return 0;
}
T3「KDOI-10」反回文串
“两个特殊性质已经把做法塞你嘴里了。”
好家伙,无敌了,有被冒犯到,像我这种只会写性质 A 的是不是可以一头创死了?
在没看特殊性质之前,我就以为答案是 \(n/2\)。虽然很容易就会发现有问题但是抛开输出方案不谈只输出答案我觉得能骗不少分。
对于特殊性质 A,如果 \(n\) 是偶数,直接 \(i\) 与 \(i+n\) 配对。如果 \(n\) 是奇数就把多出来的那个随便找个地方放就行,答案仍然是 \(n/2\)
对于特殊性质 B,假设只有两种颜色。只能将所有的分在一起。如果此时 \(2 \nmid n\) 并且 \(s _ {\lfloor \frac n 2 \rfloor}\) 为数量少的颜色时无解,或则答案为 \(1\)。当有一堆颜色时候,按照 baa...
, ...aab
和 ac
,da
这么分。答案最优为 \(n - cnt[多的那个颜色]\) 的数量。
还有一个显然的,就是不可能有两个以上的颜色同时超过 \(n/2\),那么确实是两个特殊性质已经把做法塞你嘴里了
不存在最多超过 \(n/2\) 的颜色用 A 性质,否则用 B 做法。
复杂度 \(O(\sum n)\)
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
using namespace std;
const int N = 1e5 + 5;
const int M = 3e1 + 5; //bigger than 26 is ok
int n;
char s[N];
vector<int> v[M], p[N];
int cnt[M], id[N];
void stop_ ()
{
cout << "Shuiniao" << endl;
}
void solve ()
{
cin >> n;
cin >> (s + 1);
for (rint i = 0; i < 27; i++) v[i].clear(), cnt[i] = 0;
for (rint i = 1; i <= n; i++)
{
v[s[i] - 'a'].push_back(i); //记录字母位置方便后面的便利
cnt[s[i] - 'a']++; //计数
}
int pos = -1; //特殊性质 A
for (rint i = 0; i < 27; i++)
{
if (cnt[i] > n / 2)
{
pos = i; //记录超越 n / 2 的字母类型
break;
}
}
if (pos != -1) //不满足特殊性质 A
{
if (cnt[pos] == n) return stop_(); //全是一个字母
if (cnt[pos] == n - 1) //满足特殊性质 B
{
if ((n & 1) && s[n / 2 + 1] != pos + 'a') return stop_();
cout << "Huoyu" << endl << 1 << endl << n << ' ';
for (rint i = 1; i <= n; i++) cout << i << ' ';
cout << endl;
return ;
}
}
int k;
if (pos == -1) k = n / 2; //满足特殊性质 A
else k = n - cnt[pos];
/*
一定不吹出现第二种超过 n / 2 的字母类型
所以剩下的字母个数就是答案
*/
cout << "Huoyu" << endl << k << endl;
for (rint i = 1; i <= k; i++) p[i].clear();
if (pos == -1) //满足特殊性质 A
{
int idx = 0;
for (rint i = 0; i < 27; i++)
for (rint j : v[i])
id[++idx] = j;
// 按照 26 个字母的顺序将输入的字符串存入 id
for (rint i = 1; i <= k; i++)
{
p[i].push_back(id[i]);
p[i].push_back(id[i + n / 2]);
}// 把 (i, i + n / 2) 搭配存
if (n & 1) //如果是奇数
{
p[1].push_back(id[n]);
}
}
else
// 不满足特殊性质 A
// 即存在绝对众数
{
int idx = 0, check = -1;
for (rint i = 1; i <= n; i++)
{
if (s[i] == pos + 'a') id[++idx] = i;
else
{
if (check == -1)
{
check = 0;
p[k].push_back(i);
}
else
{
check++;
p[check].push_back(i);
}
}
}
idx = 0;
for (rint i = 1; i < k; i++) p[i].push_back(id[++idx]);
while (id[idx + 1] < p[k][0]) p[k - 1].push_back(id[++idx]);
while (idx < cnt[pos]) p[k].push_back(id[++idx]);
}
for (rint i = 1; i <= k; i++) sort(p[i].begin(), p[i].end());
for (rint i = 1; i <= k; i++)
{
cout << p[i].size() << ' ';
for (rint j : p[i]) cout << j << ' ';
cout << endl;
}
return ;
}
signed main()
{
int c, T;
cin >> c >> T;
while (T--) solve();
return 0;
}
T4「KDOI-10」超级演出
首先有个非常好拿的 32pts 部分分可以拿
正解通过向 xxseven 大佬学习已经领会了。%%% xxseven
考虑在线操作,貌似并不好处理。但是可以离线处理。选出来的是连续区间,找出序列中每个位置可以走的最大的左端点,同时维护上一次出现的位置因为会出现重复的情况。同时维护最后出现的位置。没必要开数组维护,这个可以在计算时动态维护。然后计算区间贡献,现在就可以离线处理了,变成离线二维数点问题。
当前算法复杂度为 \(O(n^2)\),考虑优化算法。
发现可以根号分治一下(话说是怎么做到赛时想出来可以根号分治的)如果一个点的出边数小于等于根号,那么每次暴力找即可。出边数小于等于根号暴力找。大于的话开 vector
记录出度大于根号的前驱。那么,出边在 sqrt(m)
以上的点称为大点 这些点的 lst
值本来应该是在修改其他点的时候顺带维护的。但是我们真正修改它的时候要在树状数组里面把原来的值减掉 因此我们不能直接在维护大点时直接修改 lst
,tp[]
数组就是用来临时记录 lst
将要被修改为多少,当碰到某个命令为这个大点时,我们再去真正更新。
最后复杂度为 \(O(n \sqrt n)\)
32pts 代码
void dfs(int x)
{
vis[1] = 0;
if (!vis[root]) return;
for (rint i = 0; i < g[x].size(); i++)
{
if (g[x][i] == 1)
{
flag = 1;
vis[root] = 0;
return ;
}
if (!vis[g[x][i]]) dfs(g[x][i]);
}
}
signed main()
{
cin >> c;
cin >> n >> m >> k >> q;
for (rint i = 1; i <= m; i++)
{
int a, b;
cin >> a >> b;
g[a].push_back(b);
}
for (rint i = 1; i <= k; i++) cin >> a[i];
while (q--)
{
for (rint i = 1; i <= n; i++) vis[i] = 1;
int ans = n - 1;
int l, r;
cin >> l >> r;
for (rint i = l; i <= r; i++)
{
flag = 0;
root = a[i];
dfs(a[i]);
if (flag) ans--;
}
cout << ans << endl;
}
return 0;
}
100 pts 代码
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
using namespace std;
const int N = 5e5 + 5;
int n, m, k, q;
int c[N], tr[N], lst[N], ans[N], out[N], tp[N];
vector<int> e[N], E[N];
vector<pair<int, int> > g[N];
bool flag[N];
int lowbit(int x) {return x & -x;}
void add(int x, int y)
{
if (!x) return ;
for (; x < N; x += lowbit(x)) tr[x] += y;
}
int ask(int x)
{
int ans = 0;
for (; x; x -= lowbit(x)) ans += tr[x];
return ans;
}
signed main()
{
int C;
cin >> C >> n >> m >> k >> q;
for (rint i = 1; i <= m; i++)
{
int a, b;
cin >> a >> b;
e[a].push_back(b);
if (b == 1) flag[a] = 1; //记录可以一步到位的点
out[a]++;
}
for (rint i = 1; i <= k; i++) cin >> c[i];
for (rint i = 1; i <= q; i++)
{
int l, r;
cin >> l >> r;
g[r].push_back({l, i});
}
int t = sqrt(m);
for (rint i = 1; i <= n; i++)
{
if (out[i] < t) continue;
for (rint x : e[i]) E[x].push_back(i);
}
for (rint i = 1; i <= k; i++)
{
int j = c[i], nxt = 0;
if (out[j] >= t) nxt = tp[j];
else
{
for (rint x : e[j])
nxt = max(nxt, lst[x]);
}
if (flag[j]) nxt = i; //一步到位
add(lst[j], -1), lst[j] = nxt;
add(lst[j], 1);
for (rint u : E[j]) tp[u] = max(tp[u], lst[j]);
for (auto e : g[i]) ans[e.second] = ask(k) - ask(e.first - 1);
}
for (rint i = 1; i <= q; i++)
{
cout << n - 1 - ans[i] << endl;
}
return 0;
}