HZOI CSP-S模拟8——汤匙の爆零祭
血在前面:
《自爆家门》( "\(/\)" 后面的是后来改的):
最纯糖的一集,T1
看出单调栈和DP于是兴奋极了,抛弃单调栈,开始想DP,未果,寻病终, 后来这位唐人认为自己想到了一个比较真的做法,于是开始作法,努力搞了许久(这时已经有单调栈的雏形了,但这位\(螳臂\)依旧认为自己的逆天DP是正确的、真实的、可靠的),开始测大样例,发现疑似全过了(其实最后一个样例后面几位错了,但瞎子传奇之人眼对拍过了好久才反应过来),因做法逆天遂调试失败,交上去以为能对个一两个点,但假得很彻底;T2
一眼看出二分,看到神秘的题干描述又yy出了大根堆,但没把它俩往一块想,想到一个假的贪心,贪污腐败,甲烷了;T3
一看期望DP(快忘完了),再一看疑似高几高斯消元(泰坏辣,我不会),直接跳了;T4
一眼树剖,但人傻不会处理,且此时仅剩长达0.5h
的时间,于是努力写了俩不知真假的电风扇,最终遗憾离场。《这就是肝·螳臂·HZOI爆零王·对不起教教主·硬化·悲伤蛙一世的一上午》
然后就是非常感谢APJ学长不辞辛苦搬了这些模拟赛题,感觉思维难度和代码难度都有,都要好好想一想,但是我还是泰唐了,赛时全部想假了。
开始题解:
T1 酒吧(bar)
题面大意:(其实并不很简洁明了)
一个长度为\(n\)的序列,选择第 \(i\) 个位置可以得到 \(p_i*c_i\) 的报酬,(其中 \(p_i\) 是你输入的,\(c_i=(next-last)\) , \(last\) 是对于第 \(i\) 个位置上一次选择的位置, \(next\) 是下一个位置),使 \(∑c_i*p_i\) 最大化。
赛时错误作法做法(注:以下思路全是假的,只是作者想搞一篇类似“错题本”的博客才记录的错解,不要被误导,真要看正解的请移步“Solve”):
容易想到贪心/DP,于是可以开始乱搞。由于开头和结尾均选择一定最优,所以先将 \(dp_1\) 赋值成 \((n-1)*p_1\) ,记录一个上一次取的位置 \(last\) 以及该位置的值 \(lastp\) 和上上回取的位置 \(llast\) 以及该位置的值 \(llastp\) ,那么状态转移方程就是\(dp_i=max(dp_{last},dp_{last}-(n-i)*lastp+(n-last)*p_i,dp_{llast}-(n-i)*llastp+(n-llast)*p_i)\) ,时间复杂度 \(O(n)\) 。
于是我们就可以并非愉快地开始写了,写完可以惊奇地发现:最后一个大样例过不去。此时我们想:万一数据很蒻(比gyh还要蒻)呢?是不是能骗一点点分呢?
0.
骗我们的,这个做法是假的!学长的数据泰羟辣!!!
还记得《血在前面》中提到的单调栈吗?
↓
Solve
容易经过努力地手玩样例可以发现,开头与结尾均选择一定是最优的。设选择的位置为 \(b_1,b_,…,b_m\) ,那么答案就是\(\sum_{i = 1}^{m-1} {(b_i+1-b_i)(p_{b_i}+p_{b_i+1})}\) 。发现这个其实与梯形面积公式很像,那么如果我们将所有 \((i,p_i)\) 全部看做二维平面上的点,所求贡献就是在这里面选若干个点,使得他们的连线与 \(x\) 轴形成的封闭图形的面积最大。那么显然选一个凸包是最优的,直接单调栈求得答案即可(说人话就是该柿子满足单调性,直接维护一个单调不减的单调栈就可以)。复杂度 \(O(n)\) 。
Code
#include <bits/stdc++.h>
using namespace std;
const int _ = 500010;
int n, s[_], t;
long long dp[_], p[_], ans;
int main(){
freopen("bar.in", "r", stdin);
freopen("bar.out", "w", stdout);
scanf("%d", & n);
for(int i = 1; i <= n; i ++){
scanf("%lld", & p[i]);
}
dp[1] = (n - 1) * p[1];
s[++ t] = 1;
for(int i = 2; i <= n; i ++){
while(t > 1 && p[i] * (n - s[t]) + dp[s[t]] - p[s[t]] * (n - i) < p[i] * (n - s[t - 1]) + dp[s[t - 1]] - p[s[t - 1]] * (n - i)){
t --;
}
dp[i] = p[i] * (n - s[t]) + dp[s[t]] - p[s[t]] * (n - i);
s[++ t] = i;
ans = max(ans, dp[i]);
}
printf("%lld", ans);
return 0;
}
T2 逆转(ace)
题面大意:(其实像在说梦话)
一个长度为 \(n\) 的序列,第 \(i\) 个位置权值为 \(a_i\) ,选出一个长度为 \(k\) 的子序列,将子序列分成前后两段且不改变它们的编号顺序,将前后两段分别求它们的权值和,使最大权值和最小。
赛时错误做法:
首先动用人类智慧,结合多次做(挂)题(分)经验可以看出是二分答案,这时我们可以想到一个贪心:用一个结构体存下输入的所有数据以及它们的编号,按权值从小到大,编号从小到大 \(sort\) 一遍,再将前 \(k\) 个数存下来,并在第 \(k+1\) 到 \(n\) 个数里面查找与第 \(k\) 个数相等的数,用一个变量 \(cnt\) 记录它们的位置,然后将前 \(k\) 个数按编号再 \(sort\) 一遍,处理前缀和 \(sum_1,sum_2,…,sum_k\)。二分答案时比较 \(sum_{mid}\) 与 \(sum_{r}-sum_{mid}\) ,因为可能会有一些与第k大的数权值相同但相对于 \(mid\) 的位置不同的数,所以在二分里再套一个二分(因为赛时处理太过汤匙遂具体处理过程就不写了),如果相等就输出然后 \(break\) ,左边小于右边就让 \(l=mid+1\) ,反之就让 \(r=mid-1\) 。
之后我们就可以愉快地发现:前两个样例,过啦!!!1111
且慢!【尔康手】
-要不要看看第三个样例?
-好鸭好鸭(^ . ^)!
-你死假了。
-欸?!
正确输出:
18809
我们的输出:
19032
为什么呢?
-让我们将第三个大样例 \(sort\) 一下并输出前 \(k\) 位:
2930 6324 9778 9555
-没问题吧,最小的最大值就是19032
口牙。
-那我们再将前 \(k+1\) 位输出一下吧!
-嗯,嗯!
-2930 6324 9778 9555 10141
。
-欸, \(2930+6324+9555\) 好像确实等于 18809
呢!
-So,you will be like them abandon me. 我们又想假啦!
-怎么办呢喵?
还记得“血在前面”中提到的大根堆吗?
↓
Solve
最大值最小类问题考虑二分答案,设当前二分的答案小于等于 \(x\) ,那么我们只需要知道是否存在选择方案使得和小于等于 \(x\) 且数量大于等于 \(k\) 。题目中要求划分成两段,那么我们考虑对每个前缀,和每个后缀求出可以选择的最多的数,那么如果存在一个分界点使得可以选择的最多的数大于等于 \(k\) 那么就是合法的。
考虑如何对每个前缀快速求出答案。首先肯定是贪心的从小到大依次选,直到总和超过 \(x\) 。那么考虑插入一个数后答案的变化,如果插入这个数之后总和不会超过 \(x\) ,那么可以直接插入,否则如果当前这个数小于已选的最大值,那么将最大值换成这个数显然更优,否则肯定是插入不了的。那么我们拿一个大根堆维护即可。复杂度 \(O(nlog_nlog_v)\)。
Code
#include <bits/stdc++.h>
using namespace std;
#define re register
const int _ = 300010;
int n, k, used[_];
long long a[_], l, r, ans, sum;//不开long long见祖宗。
std::priority_queue<int> q;
inline bool check(re long long x){
while(q. size()){
q. pop();
}//原始的,愚蠢的,缓慢的清空方式(但是我因为没清空调了半年)。
sum = 0;
for(re int i = 1; i <= n; i ++){
sum += a[i];
q. push(a[i]);
while(sum > x){
sum -= q. top();
q. pop();
}
used[i] = q. size();
}//记录可选的最长前缀。
while(q. size()){
q. pop();
}
sum = 0;
for(re int i = n; i; i --){
sum += a[i];
q. push(a[i]);
while(sum > x){
sum -= q. top();
q. pop();
}
if(used[i - 1] + q. size() >= k){//"q. size()"是当前可选的最长后缀,若合法即可返回1。
return 1;
}
}
return 0;
}
int main(){
freopen("ace.in", "r", stdin);
freopen("ace.out", "w", stdout);
scanf("%d%d", & n, & k);
for(re int i = 1; i <= n; i ++){
scanf("%lld", & a[i]);
r += a[i];
}
while(l <= r){//二分答案。
re long long mid = (l + r) >> 1;
if(check(mid)){
r = mid - 1;
ans = mid;
}
else{
l = mid + 1;
}
}
printf("%lld", ans);
return 0;
}
T3 世界(world)
题面大意:(其实我不会)
可能大概或许应该好像看懂了,但是真的不会中译中。
Slove
我不会。 就是一个诡谲大柿子 \(+\) 查找神秘性质来解方程极速版最终做到复杂度由 \(O(m^6)\) 到 \(O(m^3)\) 。但是我真的不会。【悲】
Code
说了 \(inf\) 遍了,我真的不会。(Dui_Bu_Qi)
T4 染色(paint)
题面大意:
咕咕咕。
Solve
咕咕咕。
Code
这个真的有
#include<bits/stdc++.h>
using namespace std;
#define re register
const int _ = 2e5 + 5;
vector<int> g[_], v[_];
int rain, plc[_], n, q, t, deep[_], z, op, x[_], y[_], son[_], siz[_], top[_], fa[_], dfn[_], num, dis[_], lft[_], rgh[_];
inline void add(re int x, re int y, re int w){
g[x]. emplace_back(y);
v[x]. emplace_back(w);
//cout<<"???";
return ;
}
inline void dfsa(re int root){
siz[root] = 1;
son[root] = - 1;
lft[root] = ++ num;
dfn[num] = root;
for(re int i = 0; i < g[root]. size(); i ++){
// cout<<"LLL";
re int sroot = g[root][i];
if(deep[sroot]){
continue;
}
dis[sroot] = v[root][i];
fa[sroot] = root;
deep[sroot] = deep[root] + 1;
dfsa(sroot);
siz[root] += siz[sroot];
son[root] = (son[root] == - 1 || siz[sroot] > siz[son[root]]) ? sroot : son[root];
}
rgh[root] = ++ num;
dfn[num] = root;
return ;
}
inline void dfsb(re int root, re int tp){
top[root] = tp;
if(son[root]){
dfsb(son[root], tp);
}
for(int sroot : g[root]){
if(sroot != fa[root] && sroot != son[root]){
dfsb(sroot, sroot);
}
}
return ;
}
inline int lca(re int x, re int y){
while(top[x] != top[y]){
if(deep[top[x]] < deep[top[y]]){
std::swap(x, y);
}
x = fa[top[x]];
// cout<<"MI";
}
return deep[x] < deep[y] ? x : y;
}
const int len = _ >> 7;
bitset<100005> bit[_ / len + 10], ans;
inline void init(){
for(re int i = 1; i <= num; i ++){
plc[i] = i / len + 1;
bit[plc[i]][dis[dfn[i]]] = 1 ^ bit[plc[i]][dis[dfn[i]]];
}
return ;
}
inline void suan(re int l, re int r){
for(re int i = l; i <= r; i ++){
ans[dis[dfn[i]]] = 1 ^ ans[dis[dfn[i]]];
}
return ;
}
inline int check(re int l, re int r){
ans = 0;
if(plc[l] == plc[r]){
suan(l, r);
ans[0] = 0;
return ans. count();
}
// cout<<"BOOM!!!";
suan(l, plc[l] * len - 1);
suan((plc[r] - 1) * len, r);
for(int i = plc[l] + 1; i < plc[r]; i ++){
ans ^= bit[i];
}
ans[0] = 0;
return ans. count();
}
inline bool query(re int x, re int y){
// cout<<"JK";
if(x == y){
return 1;
}
re int l = lca(x, y), num = deep[x] + deep[y] - 2 * deep[l], ql = 0, qr = 0;
if(l == x || l == y){
if(deep[x] > deep[y]){
swap(x,y);
}
ql = lft[x] + 1, qr = lft[y];
}
else{
if(lft[x] > lft[y]){
std::swap(x, y);
}
ql = rgh[x], qr = lft[y];
}
return check(ql, qr) == num;
}
int main(){
freopen("paint.in","r",stdin);
freopen("paint.out","w",stdout);
scanf("%d%d%d", & n, & q, & t);
for(re int i = 1; i < n; i ++){
scanf("%d%d%d", & x[i], & y[i], & z);
add(x[i], y[i], z);
add(y[i], x[i], z);
}
deep[1] = 1;
dfsa(1);
dfsb(1, 1);
init();
for(re int i = 1; i < n; i ++){
if(deep[x[i]] < deep[y[i]]){
swap(x[i], y[i]);
}
}
// int kkk = 0;
for(re int i = 1; i <= q; i ++){
scanf("%d", & op);
if(op == 1){
re int a, b;
scanf("%d%d", & a, & b);
if(t){
a ^= rain, b ^= rain;
}
bit[plc[lft[x[a]]]][dis[x[a]]] = bit[plc[lft[x[a]]]][dis[x[a]]] ^ 1;
bit[plc[rgh[x[a]]]][dis[x[a]]] = bit[plc[rgh[x[a]]]][dis[x[a]]] ^ 1;
dis[x[a]] = b;
bit[plc[lft[x[a]]]][b] = bit[plc[lft[x[a]]]][b]^1;
bit[plc[rgh[x[a]]]][b] = bit[plc[rgh[x[a]]]][b]^1;
}
else{
re int x, y;
scanf("%d%d", & x, & y);
if(t){
x ^= rain, y ^= rain;
}
re bool as = query(x, y);
if(as){
rain = i;
// kkk++;
printf("Yes\n");
}
else{
printf("No\n");
}
}
}
//cout<<q << ' '<<kkk;
return 0;
}