Trick合集
LinkyChristian · · 个人记录
二分答案
核心思想:在一类答案具有单调性的问题中,通过二分最终的答案,将一个最值问题转化为判定问题。
本质是把一个最值问题转成判定问题
对于一些题目,而判定一个解是否存在,也可以理解成于计数有多少个解。
[Gym102354B] Yet Another Convolution 通过二分答案,把 max 变成 \sum。
抽屉原理
证明常用思想。
整除分块
当贡献仅与
放宽限制
很经典的Trick,当题目的限制难以计算和维护时,考虑是否存在一种更平凡的维护条件,而能使得题目的限制在这种条件下自然称为最优解。
https://www.luogu.com.cn/article/njid6it7
https://www.luogu.com.cn/article/q8ldewvt
差分贡献
形态1:类似
形态2:对每个最终结果为
组合意义
适用于有多项式贡献
分数计算
当题目要求输出分数,但分数的分子分母在运算中可能变得很大时:
1.找一个大于答案的质数p,将分数对p取模得到c,之后再一个个尝试分母b,根据a=cb(modp)来还原分数。
2.直接使用python的分数类型
min-max 容斥
多项式拆分
如果一个式子的构成可以被拆分成多个多项式相乘,考虑能否单独维护所有多项式。
dsu on tree
对一颗树上的信息统计,若能做到O(1)的加入数据和O(1)的删除数据,则可以使用dsu on tree
核心要点是树链,然后用一个数组统计,对于每个点每次先遍历所有轻儿子,每遍历一个轻儿子之后将该儿子子树数据删除,最后遍历重儿子,重儿子遍历出来后数据不删除,加上所有轻儿子子树的数据再加上该点本身的数据作为该点的数据。
其复杂度很好证明,每个点除开第一次被统计,只会额外在祖先链上的每条轻边被删除一次再被统计一次,而祖先链上的轻边不超过log条,所以复杂度为
CF600E Lomsat gelral
#include<bits/stdc++.h>
#define N 100010
#define pb push_back
#define int long long
using namespace std;
int n,a[N],buc[N],siz[N],son[N],mx,ans[N],mxs;
vector<int> to[N];
void dfs0(int now,int fa) {
siz[now]=1,son[now]=0;
for(int v:to[now]) if(v!=fa) {
dfs0(v,now),siz[now]+=siz[v];
if(siz[v]>siz[son[now]]) son[now]=v;
}
}
vector<int> id;
void add(int x) {buc[x]++; if(buc[x]==mx) mxs+=x; if(buc[x]>mx) mx=buc[x],mxs=x;}
void dfs2(int now,int fa) {
add(a[now]),id.pb(now);
for(int v:to[now]) if(v!=fa) dfs2(v,now);
}c
void dfs1(int now,int fa) {
for(int v:to[now]) if(v!=fa&&v!=son[now]) {
dfs1(v,now);
for(int x:id) buc[a[x]]=0;mx=mxs=0;id.clear();
}
if(son[now]) dfs1(son[now],now);
add(a[now]),id.pb(now);
for(int v:to[now]) if(v!=fa&&v!=son[now]) dfs2(v,now);
ans[now]=mxs;
}
main() {
cin.tie(0)->sync_with_stdio(0);
cin>>n;
for(int i=1; i<=n; i++) cin>>a[i];
for(int i=1; i<n; i++) {
int u,v;cin>>u>>v;
to[u].pb(v),to[v].pb(u);
}
dfs0(1,0),dfs1(1,0);
for(int i=1; i<=n; i++) printf("%lld ",ans[i]);
}
决策单调性
决策单调性表现为对于
决策单调性需要性质:相交优于包含,这条称作四边形不等式。
以最大贡献为例,即对于任意的
其与
若
每次找出
其二是单调队列法,考虑记三元组
例题:P3515 [POI 2011] Lightning Conductor
prufer序列相关
定义:在一棵无根树中,每次删除最小的叶子并将叶子相连点编号加入序列。
性质:prufer序列与无根树一一对应
性质:大小为
性质:每个结点在序列中出现的次数是其度数减一
建构:用一个指针维护目前最小的叶子,可以发现如果新出现的叶子比指针维护的叶子小,那么其可能出现的新的最小叶子为一根链,边跳边进序列即可,每个点最多被遍历两遍,复杂度
void solve1() {
for (int i = 1; i <= n - 1; i++) cin >> fa[i], deg[fa[i]]++;
for (int i = 1, p = 1; i <= n - 2; i++, p++) {
while (deg[p]) p++; // 自增找到下一个叶子结点
pf[i] = fa[p]; // 加入序列
while (i <= n - 2 && --deg[pf[i]] == 0 && pf[i] < p) // 如果产生新叶子结点且编号更小
pf[i + 1] = fa[pf[i]], i++;
}
}
还原:过程类似,不多赘述。
prufer序列还有很有趣的用途(来自oi-wiki):
至于那个多元二项式定理,脑内展开一下
一类kth-element问题
给出大小为
n 的可重集S=\{a_1,a_2,...,a_n \} ,按顺序输出子集和前k 小的子集的子集合。n,k\le 5\times10^5
首先将负数全加进答案,然后将负数取反,此时取一个负数的相反数相当于不取这个数,我们就将问题归纳到了全非负的情况。
排序。考虑在全非负的情况下,除去全不选的状况,最小的肯定是选
为什么是对的呢?显然对任意一个非起始状态的状态而言,它都有一个唯一的前驱,且这个前驱一定小于它,假设我们已经输出了前
我们放宽条件,考虑这一类 kth-element 问题,这类问题的共同特点是总方案数很大,但需要求的数的位置较为靠前。此时我们只要能构造出一种扩展方案,使得每个状态的至少一个前驱状态比它小,那么我们就能通过一个堆来维护。
Slope trick
slope trick 是一类用于维护下凸的dp函数的技巧(通常是下凸的分段一次函数)。如同所有的技巧一般,slope trick也有自己鲜明的特征,先来看一道例题。
CF713C Sonya and Problem Wihtout a Legend
给定一个有
n 个正整数的数组,一次操作中,可以把任意一个元素加一或减一。(元素可被减至负数或0 ),求使得原序列严格递增的求最小操作次数。
我们设
很容易列出转移式子:
看到转移柿子里存在绝对值函数,考虑用 slope trick 维护。先简单用数学归纳法证明下
假设
取 min 之后长这样:
绝对值函数也是下凸函数,显然两个下凸函数加在一起还是下凸函数。
函数上有很多分界点,函数在经过分界点的时候斜率会加一,因此我们可以通过维护分界点来维护整个函数。
回到这一题,我们考虑使用一个大根堆来维护所有分界点,其中堆顶是当前 dp 函数的最低点(最低点右边的分界点在每次 dp 转移的时候都将被删除,因此没必要维护)。
现在考虑加一个绝对值函数
代码也及其好写:
#include<bits/stdc++.h>
#define int long long
using namespace std;
priority_queue<int> q;
int read() {
int res=0,f=1;char ch=getchar();
while(!isdigit(ch)) f=ch=='-'?-1:1,ch=getchar();
while(isdigit(ch)) res=res*10+ch-'0',ch=getchar();
return f*res;
}
int n,ans;
signed main()
{
n=read();
for(int i=1; i<=n; i++) {
int tmp=read()-i;
q.push(tmp);
if(tmp<q.top()) {
ans+=q.top()-tmp;
q.pop();
}
}
printf("%lld",ans);
return 0;
}
一类集合计数问题
一类集合计数问题,形如
给定一个数列
A ,设一个集合S= \{ a_{b_1},a_{b_2},a_{b_3},\dots ,a_{b_k} \} ,其权值为val(S) ,求所有满足集合内所有元素进行⊕运算后值为x 的集合的权值和。
其中 ⊕ 运算是任意位运算,
这种问题我们可以用 FWT 来解决。以
这里的乘是 ⊕ 运算。
显然直接 FWT 是不可行的。我们对每个因式考虑他的 FWT。由于因式只有两项,所以其对 FWT 每一位的贡献只有两种可能,我们设其为
这里运用到一个性质,FWT 的和等于和的 FWT,我们将所有因式加在一起进行 FWT。考虑 FWT 后第
典例1:
给出
m 个长度为n 的 01 数列(每个 01 数列表示一个集合),求选出一些使其并集为S 的方案数。
或卷积,每个因式对每一位的贡献为
典例2:#310. 【UNR #2】黎明前的巧克力
异或卷积,每个因式对每一位的贡献为
拉格朗日插值优化dp
前言:拉格朗日插值作为一种多项式快速求值技巧,在一类dp函数为多项式函数的情况下,往往能够起到很好的降低复杂度的作用,而大多数时候我们所需要做的就是证明dp函数为一个多项式。
- 拉格朗日插值
用于解决给出不同的
simple proof:
考虑将该多项式拆成
n+1 个多项式的和,其中第i 个多项式在x_i 处为1 ,而在其它给定的点处为0 。那么第i 个多项式f_i(x)=\prod \limits_{j \neq i} \frac{x-x_j}{x_i-x_j} ,那么显然f(x)=\sum \limits_{i=1}^{n+1} y_i\times f_i(x) ,于是可以得出上文的式子。
模板: P4781 【模板】拉格朗日插值
我们一般用数学归纳法证明一个函数是多项式,而这需要用到以下三个浅显但重要的结论
-
一个
n 次的多项式的前缀和是一个n+1 次多项式。 -
一个
n 次的多项式的差分是一个n-1 次多项式。 -
一个
n 次的多项式与一个m 次多项式相乘可以得到一个n+m 次多项式。
当然就如同有人有能肉眼看出函数凸性的异能,如果你有能肉眼看出函数是个多项式的异能,也可以直接不用证明。
感应出函数的多项式之后其实可以不用证次数,直接放尽可能多的点进去插就好了。
例题一
CF995F Cowmpany Cowmpensation
给出一棵树,要求给每个点赋一个不超过
D 的权值,使得每个结点的权值不能超过它父亲的权值(如果它有的话)n\le 3000,D\le 10^9 。
考虑
我们现在来证明
显然对于叶子节点
观察柿子,最右边的部分是
最后的答案
#include<bits/stdc++.h>
#define N 3010
#define mod 1000000007
using namespace std;
int read() {
int res=0,f=1;char ch=getchar();
while(!isdigit(ch)) f=ch=='-'?-1:1,ch=getchar();
while(isdigit(ch)) res=res*10+ch-'0',ch=getchar();
return f*res;
}
int n,d,a[N],f[N][N];
int cnt,head[N],to[N],nxt[N];
void insert(int u,int v) {
cnt++;
to[cnt]=v;
nxt[cnt]=head[u];
head[u]=cnt;
}
void dfs(int now) {
for(int i=1; i<=n; i++) f[now][i]=1;
for(int i=head[now]; i; i=nxt[i]) {
dfs(to[i]);
for(int j=1; j<=n; j++) f[now][j]=1ll*f[now][j]*f[to[i]][j]%mod;
}
for(int i=1; i<=n; i++) f[now][i]=(f[now][i]+f[now][i-1])%mod;
}
int qpow(int a1,int a2) {
int res=1;
while(a2) {
if(a2&1) res=1ll*res*a1%mod;
a1=1ll*a1*a1%mod;
a2>>=1;
} return res;
}
int main() {
n=read(),d=read();
for(int i=2; i<=n; i++) {
int fa=read();
insert(fa,i);
} dfs(1);
// printf("%d\n",f[1][2]);
int ans=0;
for(int i=0; i<=n; i++) {
int s1=f[1][i],s2=1;
for(int j=0; j<=n; j++) if(i!=j) s1=1ll*s1*(d-j+mod)%mod,s2=1ll*s2*(i-j+mod)%mod;
ans=(ans+1ll*s1%mod*qpow(s2,mod-2)%mod)%mod;
}
printf("%d\n",ans);
}
第一道例题十分简单,帮助我们快速体验了证明dp函数是多项式函数的步骤。
同样简单的推导多项式性质的练习题:
练习1: P4463 [集训队互测 2012] calc
显然如果题目的dp式子中有前缀和,差分,累乘,且只在邻近层中转移。那么有较大概率是一道拉插优化题。事实上,这题本身也是拉插优化的一个常见类型:带有一些偏序限制的计数问题。
下面我们将讲述两道与例题一类型相似的题,不过难度有较大的提升。
例题二
P3643 [APIO2016] 划艇
为序列上的每个位置
i 赋一个为0 或在[a_i,b_i] 之间的权值,使得每个权值不为0 的点的权值都大于其前面所有点的权值。n\le 500,1\le a_i \le b_i \le 10^9
虽然说用组合数做又短又快,但是拉插作为一种无脑的做法考场上应该更容易想到(?)。
首先普及一个小技巧,当拉格朗日插值所用到的点值是连续的时候,我们可以做到
假设
假设我们想要求得点值横坐标为
预处理与最后的计算都是
线性插值&证多项式性质简单练习:
练习2: The Sum of the k-th Powers
回到正题,显然如果没有下头的区间限制,我们容易写出dp方程:设
显然我们有
预处理前缀和我们就能在
现在我们考虑将区间改成左闭右开之后离散化,这样数轴就被我们划分成了
我们改设
时间复杂度
#include<bits/stdc++.h>
#define N 2010
#define mod 1000000007
using namespace std;
int read() {
int res=0,f=1;char ch=getchar();
while(!isdigit(ch)) f=ch=='-'?-1:1,ch=getchar();
while(isdigit(ch)) res=(res<<3)+(res<<1)+ch-'0',ch=getchar();
return f*res;
}
int qpow(int a1,int a2) {
int res=1;
while(a2) {
if(a2&1) res=1ll*res*a1%mod;
a1=1ll*a1*a1%mod,a2>>=1;
}return res;
}
int n,l[N],r[N],f[N][N],mx,t[N],tn,ss[N],s[N][N],g[N],pre[N],suf[N],fac[N],inv[N];
int md(int a1) {return a1>=mod?a1-mod:a1;}
void add(int& a1,int a2) {a1=md(a1+a2);}
int Lag(int n,int k,int *y) {
if(k<=n) return y[k];
pre[0]=k,suf[n]=k-n;
int res=0;
for(int i=1; i<=n; i++) pre[i]=1ll*pre[i-1]*(k-i)%mod;
for(int i=n-1; i; i--) suf[i]=1ll*suf[i+1]*(k-i)%mod;
for(int i=0; i<=n; i++)
add(res,1ll*y[i]*inv[i]%mod*inv[n-i]%mod*(i>0?pre[i-1]:1)%mod*(i<n?suf[i+1]:1)%mod*((n-i)%2?mod-1:1)%mod);
return res;
}
int main() {
n=read(),fac[0]=inv[0]=1;
for(int i=1; i<=n; i++) fac[i]=1ll*fac[i-1]*i%mod;
inv[n]=qpow(fac[n],mod-2);
for(int i=n-1; i; i--) inv[i]=1ll*inv[i+1]*(i+1)%mod;
for(int i=1; i<=n; i++)
l[i]=read(),r[i]=read()+1,mx=max(mx,r[i]-1),
t[++tn]=l[i],t[++tn]=r[i];
sort(t+1,t+tn+1);
tn=unique(t+1,t+tn+1)-t-1;
for(int i=1; i<=n; i++)
l[i]=lower_bound(t+1,t+tn+1,l[i])-t,
r[i]=lower_bound(t+1,t+tn+1,r[i])-t;
for(int i=0; i<tn; i++) ss[i]=1;
for(int i=1; i<=n; i++) {
for(int j=l[i]; j<r[i]; j++) {
for(int k=0; k<=n; k++) add(f[j][k],md(ss[j-1]+(k>0?s[j][k-1]:0)));
s[j][0]=f[j][0];
for(int k=1; k<=n; k++) s[j][k]=md(s[j][k-1]+f[j][k]);
g[j]=Lag(n,t[j+1]-t[j]-1,s[j]);
}
for(int i=1; i<tn; i++) ss[i]=md(ss[i-1]+g[i]);
}
printf("%d",ss[tn-1]-1);
}
例题三
P8290 [省选联考 2022] 填树
给出一棵树,求有多少方案在树上选出一条链,给链上每个点赋一个
[l_i,r_i] 之间的权值,使得其最大值和最小值之差\le K ,此外再输出所有合法方案的权值和(一种方案的权值定义为选出的链的权值和)。
同样的,首先考虑没有区间的限制,我们枚举最小值
但是枚举最小值很麻烦,因此我们考虑差分。对于
那么我们可以路径的LCA处统计路径,设四个数组
同样的,我们考虑将值域分成许多段,使得每一段内每个点的状态是固定的(都能选/都不能选)或是匀速变化的(+1/-1)。跟上一题不同的是,上一题中我们移动的是一个点,而这一次我们移动的是一个区间
分段插值时如果搞不清楚分多少段,那么在复杂度允许的范围下能分多少就分多少,保证每个段为多项式即可。
复杂度
#include<bits/stdc++.h>
#define N 2010
#define mod 1000000007
#define int long long
int n,m=0,K,l[N],r[N];
using namespace std;
int read() {
int res=0,f=1;char ch=getchar();
while(!isdigit(ch)) f=ch=='-'?-1:1,ch=getchar();
while(isdigit(ch)) res=res*10+ch-'0',ch=getchar();
return f*res;
}
int cnt,head[N],to[N<<1],nxt[N<<1],L,R,a[N],b[N],c[N];
void insert(int u,int v) {
cnt++;
to[cnt]=v;
nxt[cnt]=head[u];
head[u]=cnt;
}
void add(int& a1,int a2) {a1=(a1+a2>=mod)?a1+a2-mod:a1+a2;}
int mul[N],pmul[N],tot[N],ptot[N],cnt1,cnt2;
int S(int x) {return 1ll*x*(x+1)>>1%mod;}
void dfs(int now,int fa) {
int ll=max(L,l[now]),rr=min(R,r[now]);
int len=ll>rr?0:rr-ll+1,sum=ll>rr?0:(S(rr)-S(ll-1)+mod)%mod;
pmul[now]=mul[now]=len,ptot[now]=tot[now]=sum;
for(int i=head[now]; i; i=nxt[i]) if(to[i]!=fa) {
dfs(to[i],now);
add(mul[now],1ll*pmul[now]*pmul[to[i]]%mod);
add(tot[now],(1ll*ptot[to[i]]*pmul[now]%mod+1ll*pmul[to[i]]*ptot[now])%mod);
add(pmul[now],1ll*pmul[to[i]]*len%mod);
add(ptot[now],(1ll*ptot[to[i]]*len%mod+1ll*pmul[to[i]]*sum%mod)%mod);
}
}
int pos[N];
void work(int f) {
dfs(1,0);
for(int i=1; i<=n; i++) cnt1+=mul[i]*f,cnt2+=tot[i]*f;
cnt1=(cnt1+mod)%mod,cnt2=(cnt2+mod)%mod;
}
int qpow(int a1,int a2) {
int res=1;
while(a2) {
if(a2&1) res=1ll*res*a1%mod;
a1=1ll*a1*a1%mod;
a2>>=1;
} return res;
}
int Lag(int len,int x,int *a1,int *a2) {
int res=0;
for(int i=0; i<len; i++) {
int s1=a2[i]%mod,s2=1;
for(int j=0; j<len; j++) if(i!=j) s1=1ll*s1*(x-a1[j])%mod,s2=1ll*s2*(a1[i]-a1[j])%mod;
add(res,1ll*s1*qpow(s2,mod-2)%mod);
} return res;
}
signed main() {
n=read(),K=read();
for(int i=1; i<=n; i++) {
l[i]=read(),r[i]=read();
pos[++m]=l[i],pos[++m]=max(l[i]-K,0ll),pos[0]=max(pos[0],r[i]+1);
pos[++m]=r[i],pos[++m]=max(r[i]-K,0ll);
}
sort(pos,pos+m+1),m=unique(pos,pos+m+1)-pos-1;
for(int i=1; i<n; i++) {
int u=read(),v=read();
insert(u,v);
insert(v,u);
}
for(int i=0,j; i<m; i++) {
L=pos[i],R=pos[i]+K;
for(j=0; j<n+2; j++,R++) {
if(pos[i]+j==pos[i+1]) break;
work(1),++L,work(-1);
a[j]=pos[i]+j,b[j]=cnt1,c[j]=cnt2;
}
if(pos[i]+j<pos[i+1]) cnt1=Lag(j,pos[i+1]-1,a,b),cnt2=Lag(j,pos[i+1]-1,a,c);
}
printf("%d\n%d",cnt1,cnt2);
}
稍有不同的分段插值:
练习3: P7116 [NOIP2020] 微信步数
进阶题
练习4: P5469 [NOI2019] 机器人
这题的主体其实已经是dp不是拉插了,可以采用拉插之外的方法维护多项式,拉插的话依旧是经典的分段插值。
Polya定理与Burnside引理
一、群的定义
若存在一个集合
封闭性 (Closure):
\forall a,b \in S,a·b \in S 结合律 (Associativity):
\forall a,b,c \in S, (a·b)·c=a·(b·c ) 单位元/幺元 (Identity element):
\exists e \in S ,\forall a \in S ,e·a=a·e=a 逆元 (Inverse element):
\forall a \in S, \exists b \in S,a·b=b·a=e 此处称b 为a 的逆元,记作b=a^{-1}
满足这些性质的集合与二元运算的二元组
生活中有许多常见的群,如整数加法群
群
下面开始的定理都只能作用于有限群。事实上,在同构意义下,所有有限群都同构于一个置换群(可以简单的理解成有限群就是置换群)。
二、置换
定义
有限集合到自身的双射称为置换,例如对于一个集合
该置换意为将
由于在交换置换中第一行的
大小为
置换乘法
置换
与置换
的乘积
循环置换
循环置换是一种特殊的置换,可表示为
即将
若两个循环置换没有相同元素,则称其 不相交 。
任意一个置换都可以分解为若干不相交的循环置换的乘积,例如
证明可以看做是从
置换群
大小为
对称群的子群称作 置换群 。
所有的循环置换也可以构成一类特殊的置换群,我们称其为 循环群 。
三、轨道-稳定子定理
群作用
设定一个群
表示群中的元素
如果满足
则称群
特别一提,群作用是可逆的(群中元素存在逆元),即对于
下面开始的定理中群作用的集合都为有限集。
轨道
设群
稳定子
对于元素
轨道-稳定子定理
即元素
四、Burnside引理
等价类
设群
对于一类等价类计数问题,我们可以使用Burnside引理
其中
五、Pólya定理
Burnside引理中群
一般情况下,Pólya定理使用于经典的染色问题,即将
Pólya定理为
在染色问题上,设不同构染色方案数为
解决一般的染色问题直接记下面的公式就好了。
其中
例题Part
P1446 [HNOI2008] Cards
题目中的
输入数据保证任意多次洗牌都可用这
m 种洗牌法中的一种代替,且对每种洗牌法,都存在一种洗牌法使得能回到原状态。
显然这是在告诉我们所有给出的置换构成了一个群。这启发我们想到 Burnside 引理。根据 Burnside 引理,我们只需求出其在每种置换下的不动点数量即可。考虑每一个循环只能染一种颜色,结合题目对每种颜色数量的限制,我们使用一个背包来统计方案数,设
#include<bits/stdc++.h>
#define N 65
#define pb push_back
using namespace std;
int n,n1,n2,n3,m,mod,a[N],vis[N],f[25][25],ans;
void add(int& a1,int a2) {a1=a1+a2>=mod?a1+a2-mod:a1+a2;}
int qpow(int a1,int a2) {
int res=1;
while(a2) {
if(a2&1) res=1ll*res*a1%mod;
a1=1ll*a1*a1%mod;
a2>>=1;
} return res;
}
int main() {
cin.tie(0)->sync_with_stdio(0);
cin>>n1>>n2>>n3>>m>>mod;
n=n1+n2+n3;
for(int i=0; i<=m; i++) {
vector<int> v;
if(i) for(int j=1; j<=n; j++) cin>>a[j],vis[j]=0;
else for(int j=1; j<=n; j++) a[j]=j;
for(int j=1; j<=n; j++) if(!vis[j]) {
int tot=1,tmp=a[j];vis[j]=1;
while(tmp!=j) tot++,vis[tmp]=1,tmp=a[tmp];
v.pb(tot);
}
for(int i=0; i<=n1; i++)
for(int j=0; j<=n2; j++) f[i][j]=0;
f[0][0]=1;
for(int x:v)
for(int p=n1; ~p; p--)
for(int q=n2; ~q; q--) {
if(p>=x) add(f[p][q],f[p-x][q]);
if(q>=x) add(f[p][q],f[p][q-x]);
}
add(ans,f[n1][n2]);
} printf("%d\n",1ll*ans*qpow(m+1,mod-2)%mod);
}
wqs二分相关
当贡献全为整数时,凸包相邻两点的斜率为整数,因此只需要二分整数斜率即可。有多点共线需要通过斜率算出真正答案。
注意只有二分到小数且无3点共线时可以求出具体方案。