7月做题总结
算法(知识点):
二维前缀和
矩阵乘法:一个nm的矩阵乘一个mk的矩阵,得到一个n*k的矩阵,a[i][j]表示第一个矩阵的第i行乘第二个矩阵的第j列,对应位置相乘
矩阵快速幂:多个矩阵乘法用快速幂实现
模板:
struct matrix
{
int f[105][105];
matrix(){
memset(f,0,sizeof(f));
}
matrix operator *(matrix &x){
matrix ret;
for(int i=0;i<=m+1;++i)
for(int j=0;j<=m+1;++j)
for(int k=0;k<=m+1;++k)
ret.f[i][j]=(ret.f[i][j]+f[i][k]*x.f[k][j]%mod)%mod;
return ret;
}
}ans,base;
矩阵乘法优化dp:答案不正确时要先检查一下构造的矩阵是否正确
期望dp:一般从后往前dp
状压dp枚举子集:注意枚举子集是指枚举一个集合+它的子集
模板代码:
for(int j=0;j<(1ll<<n);++j)
for(int k=j;k;k=(k-1)&j)
{
}
时间复杂度O(3^n)
珂朵莉树:主要解决含区间推平操作的问题
模板代码(P8512):
struct node
{
int l,r,t;
mutable int v;
bool operator<(const node &a) const{
return l<a.l;
}
};
set<node> s;
set<node>::iterator split(int pos)
{
set<node>::iterator it=s.lower_bound((node){pos,0,0,0});
if(it!=s.end()&&it->l==pos) return it;
it--;
if(it->r<pos) return s.end();
int dl=it->l,dr=it->r,dt=it->t,dv=it->v;
s.erase(it);
s.insert((node){dl,pos-1,dt,dv});
return s.insert((node){pos,dr,dt,dv}).first;
}
void assign(int l,int r,int t,int v)
{
set<node>::iterator itr=split(r+1),itl=split(l);
for(set<node>::iterator it=itl;it!=itr;++it)
{
//add(it->t,-((it->r)-(it->l)+1)*(it->v));
it->v=v;
}
s.erase(itl,itr);
s.insert((node){l,r,t,v});
//add(t,(r-l+1)*v);
}
时间复杂度O(nlogn)
树上启发式合并:主要解决每个点都需要遍历其整棵子树(空间无法记录所有子树信息,每次需清空数组),做法是每次先遍历其所有轻儿子(要清空),再遍历重儿子(无需清空),再遍历轻儿子加入数组中,由于每个点最多被遍历O(logn)次(到根的链上轻边最多logn条),时间复杂度O(nlogn)
模板代码(P6623)
void dfs(int u)
{
sz[u]=1;
for(int i=0;i<v[u].size();++i)
{
int d=v[u][i];
dis[d]=dis[u]+1;
dfs(d);
sz[u]+=sz[d];
if(sz[d]>sz[son[u]])
son[u]=d;
}
}
void cal(int u,int p)
{
for(int i=0;i<=20;++i)
t[a[u]%pw[i]][i]+=p;
for(int i=0;i<v[u].size();++i)
cal(v[u][i],p);
}
void getans(int u)
{
for(int i=0;i<v[u].size();++i)
ans[u]^=ans[v[u][i]];
for(int i=0;i<=20;++i)
if(t[dis[u]%pw[i]][i]%2)
ans[u]^=(1ll<<i);
ans[u]^=(a[u]-dis[u]);
}
void work(int u,int l)
{
for(int i=0;i<v[u].size();++i)
if(v[u][i]!=son[u])
work(v[u][i],0);
if(son[u]) work(son[u],1);
for(int i=0;i<v[u].size();++i)
if(v[u][i]!=son[u])
cal(v[u][i],1);
getans(u);
if(!l)
{
for(int i=0;i<v[u].size();++i)
cal(v[u][i],-1);
}
else
{
for(int i=0;i<=20;++i)
t[a[u]%pw[i]][i]++;
}
}
二进制分组
Boruvka:一般在完全图中使用,求最小生成树
模板代码(CF888G):
for(int i=1;i<=n;++i) fa[i]=i;
while(upd)
{
upd=false;
for(int i=1;i<=n;++i)
{
bes[i]=0;
mn[i]=inf;
}
for(int i=1;i<=n;++i)
{
int x=getfa(i);
pll p=query(x,a[i]);
int y=getfa(p.first);
if(x!=y)
{
if(p.second<mn[x])
{
mn[x]=p.second;
bes[x]=y;
}
}
}
for(int i=1;i<=n;++i)
if(bes[i]&&getfa(i)!=getfa(bes[i]))
{
int x=getfa(i),y=getfa(bes[i]);
ans+=mn[i];
merge(rt[x],rt[y]);
fa[y]=x;
upd=true;
}
}
需要注意的地方:
1.遇到期望dp算概率的时候(如CF1265E),如果要除以100(或除以某个数),先算出100的逆元,直接将p[i]乘上100的逆元,方便后续处理,不易出错
2.遇到循环移位,可以先把1...n转化为0...n-1,再转化回来,更好计算
题目,技巧:
期望dp
AT_abc282_g [ABC282G] Similar Permutation
设f[i][j][k][l]表示前i个中,有j个满足条件,A的第i个排名第k名,B的第i个排名第l名
这种状态主要适用于只关心相对关系(最好是相邻的),不关心绝对大小的题目,再使用二维前缀和即可
CF1151F Sonya and Informatics
期望dp+矩阵乘法(矩阵加速),注意k很大且dp方程无需判断时,可以考虑矩阵乘法
CF1265E Beautiful Mirrors
期望dp,从后向前推,考虑下一步是什么就从那里转移过来
注意有后效性,把dp方程式整理成关于f[1]的一次函数,从后向前递推可以整理成如下形式:f[1]=a[1]*f[1]+b[1],移项即可算出答案
状压DP
CF1209E2 Rotate Columns (hard version)
状压dp,这种带操作的dp,一般枚举操作的那一维,比如这道题对列操作,可以枚举列,但这道题列的数量很大,但我们发现行的数量很少,且求的是每行的最大值,我们最多只要用12列,发现我们只需要用最大值最大的n列即可
证明:如果还要用另一列,那么显然可以把那一列用到的用前n列替换。
那么我们设w[i][j]表示第i列,当前列使用的状态为j时移位的最大值,然后对于每一列先算出w,再按照状压dp的模板合并之前的状态与当前状态即可
P8189 [USACO22FEB] Redistributing Gifts G
这个题我们看到有多组询问,考虑把奶牛按品种分成两组,每组之间交换,然后相乘,这样可以O(1)解决询问
于是我们要先计算的就是几只奶牛形成几个环,环之间交换的方案数
使用状压dp,设g[i]表示状态i代表的牛之间交换的方案数,这样我们就可以由g计算f了
发现g必然由多个环组成,设h[i][j][k]表示状态i的奶牛,以j为起点,当前点为k的方案数
在h的最后一个点判断一下能不能与起点相连,如果能相连就把状态的数量放到g里面
这样还是超时了,注意到是一个环,所以哪个点做起点都无所谓,钦定标号最小的点为起点即可省掉一维
注意在把f[i]分为f[i^j]和g[j]时,为避免重复,让j必须包含i中编号最大的点即可
最短路
P5304 [GXOI/GZOI2019] 旅行者
我们可以把k个点分成两组,第一组接超级起点,第二组接超级终点,跑最短路,接下来考虑分组,可以随机化分组,也可以使用确定性的二进制分组,因为两个点的二进制不同,所以一定有一次被分到不同组,注意每次分组要把两边分别当起点跑一次
P6961 [NEERC 2017] Journey from Petersburg to Moscow
我们想知道第k大边的长度,所以我们枚举每条边,考虑让它作第k大边,把其他边减去它的长度(小于它的变成0)加上k*它的长度,跑最短路即可,这样做是对的,考虑证明,设实际排名为p
1.p>k,则会多算k+1-p的贡献,不会更优
2.p<j,则会把<k的当成k,不会更优
P2371 [国家集训队] 墨墨的等式
同余最短路模板题
AT_arc084_b [ABC077D] Small Multiple
同余最短路,将路径长度改为0-9即可
最小生成树
CF888G Xor-MST
Brovuka算法+01trie,注意trie树可以合并和相减,合并与线段树合并类似,相减记录sz为子树大小即可
平衡树
P3369 【模板】普通平衡树
P3391 【模板】文艺平衡树
树套树
P3380 【模板】树套树
这个题写树状数组套线段树会好写很多,常数也很小,查询时只需要把所有段的根找到,然后判断是全部往左走还是全部往右走即可
P3157 [CQOI2011] 动态逆序对
非传统题(构造,交互)
CF2068A Condorcet Elections
这个题其实不难,考虑一个排列和把它反过来的排列都加进去其实对答案没有影响
那我们直接让要求的两个数在排列中的相对大小关系不变,剩下的数一个正着,一个倒着即可
AT_arc145_d [ARC145D] Non Arithmetic Progression Set
有一个结论,所有在三进制中不含二的数中一定没有等差数列
然后把这些数全部加一或减一也不会出现等差数列
那我们只需要构造出一组数的和与m在模n意义下同余即可
考虑先放入三进制中不含二且末位为0的数,这样可以任意地对它们的末位加一,所以就一定会有一组数满足我们想要的条件
CF1364E X-OR
很多交互题都与随机化有关
注意到排列中有0,我们可以先找到0的位置,然后对每个数和0或一下即可
那么我们考虑如何快速找到0的位置
先考虑如何快速确定p[x]
其实只需要把它和随机16个位置的数或一下,再把这些值与起来即可
因为如果这样求出来的一个位置是1,那么这些值中的这个位置都是1,有极大的概率原数的这个位置就是1
但是如果只是这样的话,把每个数都扫一遍肯定还是无法快速找到0
我们可以先求出第一个位置,然后扫描这个数组,如果p[i]|p[j]=p[i],说明p[j]在二进制下是p[i]的子集,把p[i]替换为p[j],这样最后保留的位置就是0
斜率优化
P4072 [SDOI2016] 征途
斜率优化模板题

浙公网安备 33010602011771号