巨佬的博客:https://www.luogu.com.cn/blog/CFA-44/yi-suo-chang-jian-tao-lu https://www.luogu.com.cn/paste/8hnex65m https://www.cnblogs.com/C202044zxy/p/15126199.html
一些小点:
- 如果可以确定是每一对都要满足的偏序关系(如整个序列单调增),只要相邻的满足;
- 扫描一维,若难维护,考虑换另一维扫描;
- P6130:n 个和为 1 的随机变量,其中最小值的期望是 ,第 k 小值期望 。对于 n 个 [0,1] 之间的随机变量,第 k 小的那个的期望值是 k/(n+1)。https://notes.sshwy.name/Math/Expectation/Classic/
- 对于一个具有 k 个叶子的无根树,我们可以构造出使用 条路径覆盖完树所有边的方案,并且这个构造一定是下界。
- 差值绝对值 <=1,欧拉回路。
- 每次删长为 2 的串:奇偶染色
- 一棵 N 点二叉树与一棵 N+1 点多叉树对应,可以互转化。本质不同的二叉树数量等价于本质不同的先序遍历数量,二叉树和多叉树的先序遍历是相同的。
- 一个集合中任意两个正整数的异或的最小值一定出现在集合中大小关系上相邻的两个数之间。即:a<b<c,
min(a^b,b^c)<a^c
- 长度为 n 的序列,前缀最大值个数为 m 的方案数是 ,因为它完全等价于把 n 个数划分成 m 个圆排列,每个圆排列以其最大值为代表,按最大值从小到大的顺序在原序列上排列。
- 建出原图的反图,然后枚举 1~n,如果当前点没有被访问过就以其为起点在反图上 dfs,最后回溯的顺序就是拓扑序。
- 一个数被不超过它的数模一次,大小至少折半。
- 边双一定能定向成 scc。
- 最优化问题要取模,多半有固定策略。否则值域不算太大,取 log 即可。
- 树上 siz 题考虑以重心为根。
- 随机游走最远期望走到 sqrtN,可以通过随机打乱把 N 优化成 sqrtN。
- DP 不清楚转移顺序,直接记忆化搜索。
- 单调栈、笛卡尔树互换
- 区间内每一对数的贡献考虑支配点对。
- 拓扑排序的逆排列的字典序最小,即是原排列 reverse 后的字典序最大。https://www.luogu.com.cn/problem/P3243注意只有拓扑排序成立。
- 对区间问题考虑猜要么包含要么不交、不包含等等结论。
备忘:性能分析
g++ test.cpp -o test -pg ./test gprof ./test gmon.out
备份:防卡哈希
来源:https://codeforces.com/blog/entry/62393
struct custom_hash{ static uint64_t splitmix64(uint64_t x) { x+=0x9e3779b97f4a7c15; x=(x^(x>>30))*0xbf58476d1ce4e5b9; x=(x^(x>>27))*0x94d049bb133111eb; return x^(x>>31); } size_t operator()(uint64_t x) const { static const uint64_t FIXED_RANDOM = chrono::steady_clock::now().time_since_epoch().count(); return splitmix64(x + FIXED_RANDOM); } }; unordered_map<long long, int, custom_hash> safe_map; gp_hash_table<long long, int, custom_hash> safe_hash_table;
传入 std::pair<>
可以
struct custom_hash{ static uint64_t splitmix64(uint64_t x) { x+=0x9e3779b97f4a7c15; x=(x^(x>>30))*0xbf58476d1ce4e5b9; x=(x^(x>>27))*0x94d049bb133111eb; return x^(x>>31); } size_t operator()(std::pair<int,int> x) const { static const uint64_t FIXED_RANDOM = std::chrono::steady_clock::now().time_since_epoch().count(); return splitmix64(x.first + FIXED_RANDOM)^splitmix64(x.second + FIXED_RANDOM); } }; gp_hash_table<std::pair<int,int>, int, custom_hash> safe_hash_table;
备份:快读
namespace io { const int __SIZE = (1 << 21) + 1; char ibuf[__SIZE], *iS, *iT, obuf[__SIZE], *oS = obuf, *oT = oS + __SIZE - 1, __c, qu[55]; int __f, qr, _eof; #define Gc() (char)(iS == iT ? (iT = (iS = ibuf) + fread (ibuf, 1, __SIZE, stdin), (iS == iT ? EOF : *iS ++)) : *iS ++) inline void flush () { fwrite (obuf, 1, oS - obuf, stdout), oS = obuf; } inline void gc (char &x) { x = Gc(); } inline void pc (char x) { *oS ++ = x; if (oS == oT) flush (); } inline void pstr (const char *s) { int __len = strlen(s); for (__f = 0; __f < __len; ++__f) pc (s[__f]); } inline void gstr (char *s) { for(__c = Gc(); __c < 32 || __c > 126 || __c == ' ';) __c = Gc(); for(; __c > 31 && __c < 127 && __c != ' ' && __c != '\n' && __c != '\r'; ++s, __c = Gc()) *s = __c; *s = 0; } template <class I> inline bool gi (I &x) { _eof = 0; for (__f = 1, __c = Gc(); (__c < '0' || __c > '9') && !_eof; __c = Gc()) { if (__c == '-') __f = -1; _eof |= __c == EOF; } for (x = 0; __c <= '9' && __c >= '0' && !_eof; __c = Gc()) x = x * 10 + (__c & 15), _eof |= __c == EOF; x *= __f; return !_eof; } template <class I> inline void print (I x) { if (!x) pc ('0'); if (x < 0) pc ('-'), x = -x; while (x) qu[++ qr] = x % 10 + '0', x /= 10; while (qr) pc (qu[qr --]); } struct Flusher_ {~Flusher_(){flush();}}io_flusher_; }
板子:自然溢出逆元
llu inv(llu x){ llu y=x; for(int i=6;i--;){ y*=2-x*y; } return y; }
可以证明 。位数翻倍。
CF721C Journey
以答案代状态。
P3295 [SCOI2016] 萌萌哒
倍增。
P3251 [JLOI2012] 时间流逝
树上高消,式子化为 形式。
P3259 [JLOI2014] 路径规划
最短路带特定点的数量限制时,使用分层图最短路。
CF547D Mike and Fish
差值绝对值 <=1,欧拉回路。
CF1844G Tree Weights
求解方程可以值域倍增。先解决简单的模 2 意义下方程。
CF1870E Another MEX Problem
极小 MEX 段(即不论左端点还是右端点向内缩小了 MEX 都变小)只有 O(N) 个。
al>ar 时,l 只能找到一个 r;al<ar 时,r 只能找到一个 l。
求这些区间可以 nlogn。https://www.luogu.com.cn/problem/P9970。主席树上二分。
CF1481F AB Tree
只讲背包部分:求解多重背包的存在性问题,可以记录当前状态这一种物品还能取多少,就可以类似完全背包跑了,转移 O(1)。
template<class T> void cmax(T&a,T b){a=max(a,b);} //--- main memset(f,-1,sizeof f); f[0][0]=0; for(int i=1;i<=N;++i) if(siz[i]){ vsa[++K]=i; // 一个新物品,大小为 i,个数为 siz[i] for(int j=0;j<=N;++j){ if(~f[K-1][j]) f[K][j]=siz[i]; if(j>=i) cmax(f[K][j],f[K][j-i]-1); } }
P3980 [NOI2008] 志愿者招募
对于一个点,它会影响到接下来的点,故将所有的点串联起来。一个线段,就连接两个端点,费用为价格。
对于容量的设置,由于是“至少”,故将边权设为负,再整体平移。
如果是“至少”,用正边权即可。
cycle
给出一个 N 点环,环边有权 Ci,对于一系列点对 (ai,bi),须覆盖 ai~bi 或 bi~1~ai 两条路径的至少一条,求被覆盖的边的边权和最小值。(N<=5e5,点对数<=1e5)
喜报:CF 搬梦熊了 https://codeforces.com/problemset/problem/1996/G
最优化问题不会做?可以考虑一下最优解与不优的有什么区别。
最优情况下,应存在一条边未覆盖,可以枚举未覆盖的边(!),这样方向就定了,变为统计问题。
CF319D Have You Ever Heard About the Word?
考虑快速判断是否有 len 的操作。考虑取 O(N/len) 个关键点。相等的子串可以拆成最长公共前后缀。
P9291 [ROI 2018] Quick sort
记录一个有关排列和逆排列的求逆(还是转置?)技巧。
记初始排列为 p,1~n 的排列为 I,做的 k 个操作为 f1~k,用矩乘语言描述就是 。
左右求逆(转置?)得 。由 得 。
于是,可以对逆排列进行逆操作,然后反转操作序列。
补图连通块数
Boruvka 是可做的,但更可用的是并查集。
考虑一次性合并尽可能多的点,即找断边最少的点。可以发现此点的断边不超过 ,所以可以承受 的复杂度,并查集合并即可。
https://judge.yosupo.jp/submission/219227 用这个思路 BFS 也可以。
只有一个元素不加入:分治
N 个物品,对所有 i,求不考虑第 i 个物品时的 01 背包。
信息不可差分,考虑分治。
不加入 i,考虑加入除当前分治区间外的所有元素:
- l=r 时,当前 dp 状态为答案;
- 否则:
- 保存当前 dp 状态;
- 加入左半边元素,递归至右半边;
- 恢复 dp 状态,加入右半边元素,递归到左半边。
“麻将”()
DP 时在状态中记录 与 ,数量不超过 。
CF1110D Jongmah
P5371 [SNOI2019] 纸牌
观察到 与矩阵乘法形式,使用矩阵乘法优化。
特殊位置数量少,处理时,将式子对应位置的矩阵替换掉,然后再合并相同的。(即 ksm, mul, ksm, mul, ksm...)
P5279 [ZJOI2019] 麻将
对于状态 ,发现有无用的,则将 提出,构建自动机,做 DP 套 DP。
独立性拆分
https://www.luogu.com.cn/problem/P10197 中,max(a,b)=a+max(b-a,0)=(a+b+abs(a-b))/2。
https://www.luogu.com.cn/problem/at_agc034_d 中,abs(a-b)=max(a-b,b-a)。
凸情况下的 (min,+) 卷积
https://vjudge.net.cn/problem/Kattis-thief 或 https://loj.ac/p/6081
Slope Trick
https://www.luogu.com.cn/blog/372162/slope-trick
核心思想:对于一个凸函数(以下凸为例),尝试以如下方式来表示:记录下最左边第一段的表达式 y=kx+b,再用可重集 S 记录每次斜率增加 1 的位置横坐标。增加 k 则把坐标加入 S 中 k 次。
基本操作:
- 相加:k 相加,b 相加,S 直接合并。
- 找最值点:下凸函数一般求最小值,即找斜率为 0 的线段,用对顶堆维护。
- 前后缀 min:以前缀 min 为例。其实就是扔掉了 R 堆中的所有拐点。实现时可以只维护 L 堆。
- 平移:y=kx+b 的平移是初中数学知识。S 的平移就是在堆上打 tag。
CF713C Sonya and Problem Wihtout a Legend
严格增,以 转为不降。
设 fi,j 表示将 ai 变成 j,使得 [1,i] 的数列不降所需要的最小操作次数,那么有:
记 Fi(x) 为 fi,x,发现 Fi 下凸。
记 Gi(x) 为 。记 hi 表示 x=hi 时 Fi(x)=Gi(x)。
回视方程,发现需要快速维护 Gi。
其实对于这种分段函数且各函数都是一次函数的情况下,有一种快速的方法维护最小值:slope trick。
我们只需要维护所有左边的函数斜率小于 0 的关键点即可。用一个大根堆,此时 hi 就是堆顶,表示该点及以后斜率为零。
加一个绝对值函数时,会有左侧斜率减少一的情况,堆顶表示该点及以后斜率为 -1。
讨论 h(i-1) 与 ai 的关系:
- h(i-1)<=ai,直接将 ai 加入关键点,ai 就是堆顶;(这里不插入两次是因为右端斜率大于 0,不需要)
- 否则,产生贡献 h(i-1)-ai(因为最优决策点在 h(i-1)),插入两次ai(因为绝对值函数),此时 ai 处斜率为零,故弹出 h(i-1)。
CF1534G A New Beginning
一个点 P 到一条路径(这条路径的点满足纵坐标随横坐标单调递增,即只能向上向右走)的切比雪夫距离最小肯定是:经过这个点的斜率为 -1 的直线,与路径的交点 Q 间的距离。
考虑将点以 x+y 为序排列好。这样的话,一条路径总会按序经过每个点所在的斜率为 -1 的直线。于是做一个暴力的 DP,即设 fi,x 为当前 DP 到第 i 个点,当前横坐标为 x 的最小代价。
其中 。
首先是这个 min,观察发现是右部向右平移 ,即所有拐点向右移。打 lazytag。
剩下的直接做。
P3642 [APIO2016] 烟火表演
记 fu,x 表示 u 的子树内相对 u 的引爆时间均为 x 的最小代价。
向上合并时发现父边会使函数整体右移,但可以通过修改使代价减小。详见题解。
剩下的可并堆。
减半警报器
一个很厉害的 Trick,一般通过将信息均摊至若干部分,以一只 log 的代价去掉一维限制。
这种题目一般是你需要维护一个数据结构,初始给定了一些范围,每个范围有权值。每次把包含一个点的所有范围都减去 x。你需要维护每个范围被减到 <0 的最早时刻。
原理:。
P7603 [THUPC2021] 鬼街
1e5 内质因数个数不超过 6 个。先考虑质数怎么做,直接在“灵异事件”时修改,并查询,都是单点的。
将每个监视器的 y 均摊到被监视的位置上。当成质数来做。假如说其中一个点小于等于 0 了,我们可以相应的看看另一个点有没有到 0,如果没有,就把另一个点的权值拿出来,接着分。容易发现,这样每一次活动最多只会触发 logV 次警报,这样就能把两两限制拆开了。
需要维护一个能够支持全局减,维护最小值,删除最小值的数据结构。用堆来维护。
namespace mth{ int prc,prm[100005],mnp/*最小质因子*/[100005]; bool isp[100005]; void init(int n){ ... } } namespace xm{ std::priority_queue<std::pair<ll,int>, std::vector<std::pair<ll,int> >, std::greater<std::pair<ll,int> > > q[100005]; std::vector<std::vector<std::pair<ll,int> > > h; std::vector<ll> lim; std::vector<int> newring; ll a[100005]; bool vis[100005]; void ring(int x){ // 对一个警报器更新,并平均分配 for(auto&pair:h[x]){ lim[x]-=a[pair.second]-pair.first; pair.first=a[pair.second]; } if(lim[x]<=0){ vis[x]=1; newring.push_back(x+1); return; } ll tmp=(lim[x]+(int)h[x].size()-1)/(int)h[x].size(); for(auto pair:h[x]) q[pair.second].emplace(tmp+a[pair.second],x); }void add(int x,ll k){//对一个点更新 a[x]+=k; while(q[x].size()){ auto pair=q[x].top(); if(pair.first<=a[x]){ q[x].pop(); if(!vis[pair.second]) ring(pair.second); }else break; } }void _main(){ int N,Q; scanf("%d%d",&N,&Q); mth::init(100000); ll lastans=0; while(Q--){ ll y; int op,x; scanf("%d%d%lld",&op,&x,&y); y^=lastans; if(!op){ for(int pre=0;x>1;){ if(mth::mnp[x]!=pre) add(mth::mnp[x],y); pre=mth::mnp[x]; x/=mth::mnp[x]; } printf("%lld",lastans=(ll)newring.size()); if(newring.size()) std::sort(newring.begin(),newring.end()); for(int ele:newring) printf(" %d",ele); putchar(10); newring.clear(); }else{ std::vector<std::pair<ll,int> > hele; for(int pre=0;x>1;){ if(mth::mnp[x]!=pre) hele.emplace_back(a[mth::mnp[x]], mth::mnp[x]); pre=mth::mnp[x]; x/=mth::mnp[x]; } lim.push_back(y); h.push_back(hele); ring((int)h.size()-1); } } } }
2019-2020 ICPC Asia Hong Kong Regional Contest I. Incoming Asteroids
题意和上题没什么区别。
2022 CCPC Mianyang Onsite B. Call Me Call Me
你可以邀请 N 个人来参加会议,第 i 个人会参加会议当且仅当在 [li,ri] 中至少有 ki 个人参加会议,问最多有多少人参加会议。(N<=4e5,15 seconds)
肯定是每次邀请能邀请的,直接模拟这个过程。数据过水,直接过了
看起来类似,但是这次每个人的地点并不是常数个,不行。
若所有区间都是一段前缀 [1,ri],容易直接用线段树。
考虑将一段区间分成两段。假设所有区间都经过某一点 p,那么可以在 p 点将所有区间全部断开,这样就得到了一堆前缀和一堆后缀。将限制分给前缀和后缀,按照前缀后缀的长度排序,每次修改的就是一段连续区间了。
一般情况下,可以直接分治。发现类似线段树。两个 log。
2019 Summer Petrozavodsk Camp, Day 2: 300iq Contest 2 (XX Open Cup, Grand Prix of Kazan) F. Fast Spanning Tree
给你 N 个点,点 i 有点权 ai,有 M 个三元组 (si,ti,wi),不断进行以下操作:找出最小的一个 i,满足 si,ti 不连通且 si,ti 所在的连通块的点权和不少于 wi;若存在 i,则联通 si,ti;否则,停止操作。你需要输出最后一共联通了多少,以及联通的顺序。(N,M<=3e5)
假设一条边两边的权值和分别是 x,y,把边的限制(上界)平均分别丢到 x,y 上,拿一个小根堆维护 x+(s−x−y)/2。
当启发式合并两个联通块 u,v(siz[u]<siz[v])时,a[v]+=a[u]
,把u的小根堆合并进去然后查看堆顶,如果 a[v]>=堆顶的限制,那么说明 v 已经达到堆顶那条边的限制之一了,假设堆顶那条边是 (x,y,s), 那么只需要判断如果 a[x]+a[y]>=s 就加入答案,否则把剩下的 s−a[x]−a[y] 再平均分配到 x,y 两个点上面的堆里,作为新的限制。
即新的上界限制是 a[x]+(s−a[x]−a[y])/2,y 同理。
namespace ufs{ int f[100005],s[100005]; void init(int n){ for(int i=1;i<=n;++i){ f[i]=i; s[i]=1; } }int fd(int x){ return x==f[x]?x:f[x]=fd(f[x]); } } namespace xm{ struct EDGE{ int s,t,w; } edge[200005]; std::priority_queue<std::pair<int,int>, std::vector<std::pair<int,int> >, std::greater<std::pair<int,int> > > h[100005]; std::priority_queue<int,std::vector<int>,std::greater<int> > q; int lab[100005],ans[200005]; void upd(int eid){ int u=ufs::fd(edge[eid].s),v=ufs::fd(edge[eid].t); if(u==v) return; if(lab[u]+lab[v]>=edge[eid].w){ q.push(eid); return; } int w=(edge[eid].w-lab[u]-lab[v]+1)/2; h[u].emplace(lab[u]+w,eid); h[v].emplace(lab[v]+w,eid); }void un(int eid){ int u=ufs::fd(edge[eid].s),v=ufs::fd(edge[eid].t); if(u==v) return; if(ufs::s[u]>ufs::s[v]) std::swap(u,v); ufs::f[u]=v; ufs::s[v]+=ufs::s[u]; ans[++*ans]=eid; lab[v]=min(lab[u]+lab[v],(int)1e6); if(h[u].size()>h[v].size()) std::swap(h[u],h[v]); while(h[u].size()){ if(h[u].top().first<=lab[v]) upd(h[u].top().second); else h[v].push(h[u].top()); h[u].pop(); } while(h[v].size()&&h[v].top().first<=lab[v]){ upd(h[v].top().second); h[v].pop(); } }void _(){ int N,M; scanf("%d%d",&N,&M); ufs::init(N); for(int i=1;i<=N;++i) scanf("%d",lab+i); for(int i=1;i<=M;++i) scanf("%d%d%d",&edge[i].s,&edge[i].t,&edge[i].w); for(int i=1;i<=M;++i) upd(i); while(q.size()){ int p=q.top(); q.pop(); un(p); } printf("%d\n",*ans); for(int i=1;i<=*ans;++i) printf("%d ",ans[i]); putchar(10); } }
二维数点问题
一般用扫描线解决。
比如矩形加查。
P3242 [HNOI2015] 接水果
树上的一条路径 转化为点 。路径包含其它路径转化为点被矩形包含(见题解),套整体二分,转化为矩形加单点查。
V-E=1 容斥
原理:树上边数等于点数减一。
与链交结合
N 点树上 M 条路径作“毛毛虫”,Q 次询问,问两点间路径所经过(指节点)的毛毛虫权值的和。(N,M,Q<=3e6,5s)
“毛毛虫”上的点加上正的权,边加上负的权即可。
树上差分。
O(nlogn+n),瓶颈只在 LCA。
连通块计数“代表”
连通块计数问题,要确定一个连通块的“代表”以及恰当的父子关系。
cloud
给定两序列 a,b,分别长为 N,M,构造一 N×M 矩阵 c,ci,j=ai+bj,给定 X,ci,j<=X 则标记该元素,求所有被标记元素的连通块个数。(N,M<=2e5)
https://www.luogu.com.cn/problem/CF1548E 我 tm 几个月前居然做过???
统计连通块,找代表。由于性质过好,找整个连通块中 ci,j 最小的,有相同则令 i,j 最小,一定存在一个最小元素满足其他最小元素的 i,j 都不小于它。
对于两个序列,分别求对于所有 i,左方第一个 <=a/bi 的 prea/bi,与右方第一个 <a/bi 的 nxta/bi。
一个 i,j 要被统计,首先必须被标记,其次它到 prei 以及 nxti 必须断开。
一些移项。
其反面
定义这个“断开要求”为 。b 同理。
故有条件:
ST 表预处理 ma,mb,然后二维数点。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!