觉得天天改完抽象模拟赛的抽象题有点浪费时间,怪东西太多的打算直接扔,马上省选了改出来场上也不会,最近自己翻了点简单题做,迎合一下打部分分的策略,顺便提供一点信心。
品酒大会
发现 "r相似" 的定义本质就是parent树上两点lca的 \(len_u=r\),考虑对原串建出后缀自动机,问题等价于求parent树上所有节点 \(u\) 的子树任取两个不在同一枝杈上的点的方案数和最大点权乘积,前者可以用 \(siz\) 进行treedp得到。再看最优化如何实现,因为点权可以有负数,所以维护每个节点子树的最大次大最小次小值treedp即可。
对每个节点完成treedp后答案仅会被汇总到 \(len_u\),又显然 \([1,len_u]\) 中这些答案都是合法的,最后从后往前更新一遍即可。
zbo
把问题转换成:将一个节点染黑&查询一个(黑)点到所有黑点的距离和。这个题前两天才考过,考虑树剖维护换根时边的贡献从根换到查询节点即可。
老C的键盘/SAO
特殊有向图的拓扑序计数。考虑treedp,设 \(dp_{u,i}\) 表示节点 \(u\) 在当前子树中拓扑序为 \(i\) 的方案,合并 \(x,y\rightarrow dp_{x,a},dp_{y,b}\) 时考虑钦定 \(x,y\) 的先后顺序,\(x>y\) 则所有 \(x\) 之后的大于 \(y\),\(y\) 之前的小于 \(x\),上下界就确定了。加入合并完 \(x\) 拓扑序为 \(v\) 则从 \(v-1\) 里头选 \(i-1\) 个本来就比他大随便安排一下顺序剩下就是 \(y\) 的那部分,再从所有点里扣掉 \(v\) 个来和原本就比 \(x\) 小的 \(siz_x-i\) 个一起搓新的比 \(x\) 小的。前缀和优化一下能过后者。
旅行
树上查询问题,一般有链剖分,dp和优化dp和离线啥的做法。
考虑链的情况设 \(dp_{i,0/1}\) 表示到 \(i\) 是在路上还是水上,设 \(v1,v2\) 是陆路和水路就有
然后这个方程可以广义矩阵乘法维护
然后树上倍增维护一下向上和向下的转移矩阵,注意一下细节即可。
Jobs
从根往下跑,最优解里取负的说明下面有收益,考虑维护有正收益的子树二元组 \((u,f_u)\),\(f_u\) 表示的是需要有这么多的入场费才能取得收益。暴力是 \(O(n^2\log n)\)。发现这些二元组是可以自下向上合并的,你对每个点维护一个堆存一下用 \(f_v\) 为第一关键字存它能到的儿子,合并的时候先把空的自己塞进去,然后会扩展出若干个能到达的子节点,这些节点已经有入场费了,你如果发现当前的钱不够就把缺的部分叠加到 \(u\) 的入场费上,然后继续扩展,每次扩展相当于是把一个父节点的 \(f\) (包括 \(u\))拿出来然后替换成子节点塞到 \(u\) 里头,所以启发式合并这些堆,这一段的收益和新入场费叠加直到当前收益比入场费大就可以走人了,最后 \(u\) 的入场费是算完的 \(f_u\),收益是跑完子树的总收益 \(cur\) 减去 \(f_u\),把这个变成新的 \(val_u\) 给它的父节点直接当单点贡献整个合并就对了。
#include<bits/stdc++.h>
#define int long long
#define MAXN 300005
#define pii pair<int,int>
#define mp make_pair
#define fi first
#define se second
using namespace std;
const int inf=9e18;
int n,S;
struct node{int v,nxt;
}edge[MAXN<<1];
int h[MAXN],tmp;
inline void add(int u,int v){edge[++tmp]=(node){v,h[u]};h[u]=tmp;
}
int Val[MAXN],fa[MAXN],rt[MAXN],sav[MAXN];
priority_queue<pii,vector<pii>,greater<pii> >Q[MAXN],q;
inline int merge(int x,int y){if(Q[x].size()<Q[y].size())swap(x,y);while(!Q[y].empty())Q[x].push(Q[y].top()),Q[y].pop();return x;
}
int f[MAXN];
inline void dfs(int u){for(int i=h[u];i;i=edge[i].nxt){int v=edge[i].v;dfs(v);}Q[rt[u]].push(mp(f[u],u));int now=0;while(!Q[rt[u]].empty()){pii T=Q[rt[u]].top();Q[rt[u]].pop();int v=T.se,w=T.fi;if(now<w)f[u]+=w-now,now=w;if(u==v){now+=Val[u];for(int i=h[u];i;i=edge[i].nxt){int tar=edge[i].v;Q[rt[u]].push(mp(f[tar],tar));}} else{now+=Val[v];rt[u]=merge(rt[u],rt[v]);}if(now>=f[u]){Val[u]=now-f[u];return ;}}f[u]=inf;
}
signed main(){scanf("%lld%lld",&n,&S);for(int i=1;i<=n;i++){scanf("%lld%lld",&Val[i],&fa[i]);add(fa[i],i);rt[i]=i;sav[i]=Val[i];}dfs(0);int ans=S;q.push(mp(f[0],0));while(!q.empty()){pii T=q.top();q.pop();int u=T.se,w=T.fi;if(ans<f[u])break;ans+=sav[u];for(int i=h[u];i;i=edge[i].nxt){int v=edge[i].v;q.push(mp(f[v],v));}}printf("%lld\n",ans-S);return 0;
}
CF1270G
喵喵题,把条件变换一下变成 \(1\le i-a_i\le n\),然后直接让每个点连边 \(i\rightarrow i-a_i\),会形成一个基环森林,对于这上面的一个环 \(S\) 就满足
找个环就行了,真神奇。
Uplifting Excursion
第999AC。
想到正解没发现性质,有点唐。
可以考虑先allin再调整,现在结果比L大就从高到低扣正的,否则就从低到高扣负的,不难发现这样跑完答案肯定离 \(L\) 不远,事实上只要 \(L\) 在所有正allin和负allin形成的区间之中(可能有解),则这波操作下来得到的初步值 \(V\) 满足 \(V\in[L-m,L+m]\),口胡证明就是对于任何超出这个区间的我都可以加或者减几个这些元素给你拉进去。
之后考虑如何取舍第一步完了之后的集合,考虑背包,初值0给到 \(S-V\) 只要得到 \(dp_0\) 的结果即可,背包的值域因为 \(V\) 的值域所以是 \([L-m^2,L+m^2]\) 的,写一个二进制优化的完全背包即可。
CF2018B
让这题给卡住了我还活什么啊。答案只能是一段连续区间,感性理解。策略的话,用某种方法确定起点后每次去找最小的 \(a_i\) 去扩展,这样显然是对的,考虑操作交换后对大的没变化对小的更急迫。进而答案就应该是所有 \([i-a_i+1,i+a_i-1]\) 的交,特别地如果两个 \(x\le a_i\) 间的距离大于 \(a_i\) 那怎么说都是过不去的,特判一下。
CF771E
傻逼克高手。显然要在 \(m=2\) 上做文章,转移分为单行和双行的,开三个数组分别统计,拿map可以跑出来所有和为0的段,记录一下三种转移离 \(i\) 最近的转移点 \(loc_{typ,i}\),设 \(dp_{x,y}\) 为两行填到 \(x,y\) 的答案就有一个很简单的方程
但是这样很明显是死掉的,考虑优化这个暴力:想这样一个事情,既然 \(loc_{typ,i}\) 指的是这种转移方式的最近转移点,那对于一个 \(loc_{1,x}>loc_{2,y}\) 向 \(dp_{x,y}\) 的转移,在两行都填充情况下前者显然不劣于后者,而在只填充一行的情况下两者增量一致,所以就可以每次直接挑 \(loc\) 大的那个转移。这样一个看似小剪枝之后直接就对了,你从记搜的角度考虑,从 \(dp_{n,n}\) 开始每次转移只会有 \(O(1)\) 个可用状态(\(t\) 的那个状态因为满共就 \(n\) 个所以很快就会被记忆化),复杂度直接就是 \(O(n)\) 了。
CF1819D
考虑使用一些手法最大化一个后缀不清空段,这样肯定是最优的;后缀不清空段是还有0还能直接打满。但是倒着求这个段好像有点扯。
考虑正着跑一个 \(dp_i\) 表示到 \(i\) 的最长后缀不清空段的起点位置,显然每个 \(i\) 中元素 \(w_{i,j}\) 的 \(lst_{w_{i,j}}\) 会把这个dp往后顶,记最终顶到的位置是 \(x\),但如果我在这中间有机会清空一次的话就可以让这个往后顶的亏损小一点,开一个 \(tag_i\) 表示 \(i\) 是否能被清,维护一个指针 \(loc\) 表示当前这个 \(dp_i\) 显然往前顶的过程中一旦找到一个 \(tag_{loc}=1\) 哪怕有 \(loc<x\) 也不用管,清了你怎么影响我。还有一个办法就是:因为我肯定能早清就早清,所以如果当前 \([loc+1,i]\) 没有矛盾但是这中间有一个 \(loc+1\le y\le i\) 的 \(0\) 其实 \([y,i]\) 这一段都可以清空,这一段不用暴力赋值只用把当前的 \(i\) 标记了就行了因为这段区间的每个元素都会满足这个条件。
[CERC2014] Parades
有意思的treedp。
显然要在节点儿子个数<10做文章。考虑直接设 \(dp_u\) 表示节点 \(u\) 子树的答案,把所有的路径挂到它们的lca上处理,则选择一条路径 \((x,y)\) 的贡献应该是
其中 \(con(x,y)\) 是这条路径上那些子树提供的答案。
但是这样不好合并,无法处理多个不影响路径之间贡献的叠加,这时候就要考虑那个很小的儿子个数了,状态压缩:设 \(f_{u,S}\) 表示节点 \(u\) 的儿子节点选取状态为 \(S\) 时的答案,给每个儿子编个号上面的贡献对象就变成了 \(f_{u,x|y}\),单个链处理完之后直接枚举子集就行了。
看 \(con(x,y)\) 怎么计算,可以用线段树/树状数组维护区间加单点查。比如当前dp到 \(u\),从前在dp完一个子树节点 \(u'\) 的时候对于它的一个儿子 \(v\) 的子树,届时在 \((v,u)\) 这条路上应该会多出 \(f_{u',U-v}\) 的贡献(就是给儿子刨掉的剩下一坨接到这条链上),对 \(v\) 子树全部做一个这样贡献的加。然后dp是从叶子跑到根的,这样跑上去答案正好变加边统计,喵喵的。
软件安装
依赖关系不一定是树状的,缩完点跑treedp即可。
我怎么一直在做treedp???
城市建设
这个可以cdq做,但是我不会。
长得特别lct维护最小生成树,问题是随着边权的改变会虚空索边,然后就死了。就可以考虑用线段树分治来维护,每次修改操作相当于在原先那条边对应的链表上新加入一条边,边权 \(w\) 改成输入的那个,这样的话每个边就能在线段树上维护生存周期,用lct维护mst的同时处理额外进区间时的加边删边即可。想一想这个过程确实是可撤销的且复杂度 \(O(n\log^2n)\)。
话是这么说的但是细节有点小多,这两个东西随便锅一下都要调好久...主要是没做过线段树分治加dsu以外数据结构的题。
CF1762F
显然根据 \(l,r\) 的大小只要搓一个单增或者单减的序列就可以了。然后从大到小扫所有的权值 \(w\),则 \([w,w+K]\) 这一段的位置是可以set+双指针跑出来的,对每个权值的对应位置 \(i\) 就可以拿set二分一下查出来一个可以用来继承的后继 \(nxt_i\)。
有了这个后继之后就比较好办了,从后往前扫一下原序列,对于当前要处理的 \(i\) 和已经算完的 \(nxt_i\),显然有 \(dp_{i}\leftarrow dp_{nxt_i}\),然后对于 \(i\le x\le n,a_i\le a_x\le a_{nxt_i}\) 的所有 \(x\) 相当于不会在 \(nxt_i\) 中被计算到,把符合这样的 \(x\) 加进去就行了,这个你可以写一个bit。
魔法森林
lct维护最小瓶颈生成树板子。按 \(a\) 排序后逐个加入边的 \(b\) 即可。
CF2018F3
上面那个2018B的后继题目。根据结论合法的位置是一个连续段,设 \(dp_{l,r}\) 表示 \([l,r]\) 都合法的方案数,则一组恰好就是 \(\sum_{r-l+1=K}dp_{l,r}-dp_{l-1,r}-dp_{l,r+1}+dp_{l-1,r+1}\),相当于一个二维前缀和。然后B题的策略是每次往最紧张的那个点扩张,所以设 \(g_{0/1,l,r}\) 表示现在扩展到 \([l,r]\) 且是否被钦定向一个方向(右)走。
钦定,走完还得走:
钦定,走完正好到那个位置了
不钦定写成 \(g_{0,l,r}\) 就行了,初值 \(g_{1,l,r}=g_{0,l,r}=1\),相当于直接从这块开始。注意 \([l,r]\) 内元素的安置是没有被考虑的,所以再乘上一个内部的限制
但是这样复杂度是死掉的,因为你一直在枚举一个 \([l,r]\),然后就考虑优化,用了一个叫反推贡献系数的东西,就是重定义 \(g_{0/1,l,r}\) 表示dp完对这个位置的答案贡献,然后转移就会反过来,初值是 \(g_{0,1,n}=1\),就有
后半部分可以预处理,前半部分会变成 \(O(n^2)\)。