Stars_visitor_tyw
2024-06-04 21:16:39
此文章是本人学习算法时的学习笔记。
部分知识点从本人专栏收录。
分治。
手造一段数列:
2 5 9 1 7 6 5 3
给定
type1. 将区间
type2. 询问区间
下图详解。
位置不够,画的有点混乱了。
每个点上方的红色数字代表
建一棵线段树
查询函数
修改函数
线段树结点编号达到
build
query
update
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+5;
int a[N],tree[4*N], n, m;
void pushup(int cur)
{
tree[cur]=tree[2*cur]+tree[2*cur+1];
return ;
}
void build(int cur, int lt, int rt)
{
if(lt==rt)
{
tree[cur]=a[lt];
return ;
}
int mid=(lt+rt)>>1;
build(cur*2,lt,mid);
build(cur*2+1,mid+1,rt);
pushup(cur);
return ;
}
int query(int cur, int lt, int rt, int qx, int qy)
{
if(qy<lt||qx>rt)
{
return 0;
}
if(qx<=lt&&rt<=qy)
{
return tree[cur];
}
int mid=lt+rt>>1;
return query(cur*2,lt,mid,qx,qy)+query(cur*2+1,mid+1,rt,qx,qy);
}
void update(int cur, int lt, int rt, int qx, int qy, int val)
{
if(qy<lt||qx>rt)
{
return ;
}
if(lt==rt)
{
tree[cur]+=val;
return ;
}
int mid=lt+rt>>1;
update(cur*2,lt,mid,qx,qy,val);
update(cur*2+1,mid+1,rt,qx,qy,val);
pushup(cur);
}
signed main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
build(1,1,n);
while(m--)
{
int opt, x, y, val;
cin>>opt>>x>>y;
if(opt==1)
{
cin>>val;
update(1,1,n,x,y,val);
}
if(opt==2)
{
cout<<query(1,1,n,x,y)<<"\n";
}
}
}
很容易发现,单次修改的时间复杂度很慢。
考虑优化,采用懒标记。
性质:线段树的修改是为询问而服务的。
维护标记
修改后 update
函数单次时间复杂度可达
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int a[N], n, m, tag[4*N], tree[4*N];
void pushup(int cur)
{
tree[cur]=tree[cur*2]+tree[cur*2+1];
return ;
}
void addtag(int cur, int lt, int rt, int val)
{
tag[cur]+=val;
tree[cur]+=(rt-lt+1)*val;
return ;
}
void pushdown(int cur, int lt, int rt)
{
if(tag[cur]==0)
{
return ;
}
int mid=lt+rt>>1;
addtag(cur*2,lt,mid,tag[cur]);
addtag(cur*2+1,mid+1,rt,tag[cur]);
tag[cur]=0;
return ;
}
void build(int cur, int lt, int rt)
{
if(lt==rt)
{
tree[cur]=a[lt];
return ;
}
int mid=lt+rt>>1;
build(cur*2,lt,mid);
build(cur*2+1,mid+1,rt);
pushup(cur);
return ;
}
int query(int cur, int lt, int rt, int qx, int qy)
{
if(qy<lt||qx>rt)
{
return 0;
}
if(qx<=lt&&rt<=qy)
{
return tree[cur];
}
pushdown(cur,lt,rt);
int mid=lt+rt>>1;
return query(cur*2,lt,mid,qx,qy)+query(cur*2+1,mid+1,rt,qx,qy);
}
void update(int cur, int lt, int rt, int qx, int qy, int val)
{
if(qy<lt||qx>rt)
{
return ;
}
if(qx<=lt&&rt<=qy)
{
addtag(cur,lt,rt,val);
return ;
}
pushdown(cur,lt,rt);
int mid=lt+rt>>1;
update(cur*2,lt,mid,qx,qy,val);
update(cur*2+1,mid+1,rt,qx,qy,val);
pushup(cur);
return ;
}
signed main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
build(1,1,n);
while(m--)
{
int opt, x, y, val;
cin>>opt>>x>>y;
if(opt==1)
{
cin>>val;
update(1,1,n,x,y,val);
}
else
{
cout<<query(1,1,n,x,y)<<"\n";
}
}
return 0;
}
多维护一个乘法标记,随加法标记更新,注意运算顺序。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int a[N], n, m, tag[4*N], tree[4*N], mod, mul[4*N];
void pushup(int cur)
{
tree[cur]=tree[cur*2]%mod+tree[cur*2+1]%mod;
return ;
}
void addtag(int cur, int lt, int rt, int val)
{
tag[cur]+=val;
tree[cur]+=(rt-lt+1)*val%mod;
return ;
}
void addtag1(int cur, int lt, int rt, int val)
{
tag[cur]=tag[cur]*val%mod;
mul[cur]=mul[cur]*val%mod;
tree[cur]=tree[cur]*val%mod;
return ;
}
void pushdown(int cur, int lt, int rt)
{
if(tag[cur]==0&&mul[cur]==1)
{
return ;
}
int mid=(lt+rt)>>1;
addtag1(cur*2,lt,mid,mul[cur]);
addtag1(cur*2+1,mid+1,rt,mul[cur]);
addtag(cur*2,lt,mid,tag[cur]);
addtag(cur*2+1,mid+1,rt,tag[cur]);
tag[cur]=0;
mul[cur]=1;
return ;
}
void build(int cur, int lt, int rt)
{
if(lt==rt)
{
tree[cur]=a[lt];
return ;
}
int mid=lt+rt>>1;
build(cur*2,lt,mid);
build(cur*2+1,mid+1,rt);
pushup(cur);
return ;
}
int query(int cur, int lt, int rt, int qx, int qy)
{
if(qy<lt||qx>rt)
{
return 0;
}
if(qx<=lt&&rt<=qy)
{
return tree[cur];
}
pushdown(cur,lt,rt);
int mid=lt+rt>>1;
return query(cur*2,lt,mid,qx,qy)+query(cur*2+1,mid+1,rt,qx,qy);
}
void update(int cur, int lt, int rt, int qx, int qy, int val)
{
if(qy<lt||qx>rt)
{
return ;
}
if(qx<=lt&&rt<=qy)
{
addtag(cur,lt,rt,val);
return ;
}
pushdown(cur,lt,rt);
int mid=lt+rt>>1;
update(cur*2,lt,mid,qx,qy,val);
update(cur*2+1,mid+1,rt,qx,qy,val);
pushup(cur);
return ;
}
void update1(int cur, int lt, int rt, int qx, int qy, int val)
{
if(qy<lt||qx>rt)
{
return ;
}
if(qx<=lt&&rt<=qy)
{
addtag1(cur,lt,rt,val);
return ;
}
pushdown(cur,lt,rt);
int mid=lt+rt>>1;
update1(cur*2,lt,mid,qx,qy,val);
update1(cur*2+1,mid+1,rt,qx,qy,val);
pushup(cur);
return ;
}
signed main()
{
cin>>n>>m>>mod;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
for(int i=1;i<=4*n;i++)
{
mul[i]=1;
}
build(1,1,n);
while(m--)
{
int opt, x, y, val;
cin>>opt>>x>>y;
if(opt==2)
{
cin>>val;
update(1,1,n,x,y,val);
}
else if(opt==1)
{
cin>>val;
update1(1,1,n,x,y,val);
}
else
{
cout<<query(1,1,n,x,y)%mod<<"\n";
}
}
return 0;
}
统计一段区间
若有
每种字母维护一棵关于下标的线段树,数值为
询问时,对
修改时,对每一种字母做区间修改,最多
指线段树的结点管辖的不是一段下标,而是一段值域。
通俗点讲,就是,
线段树的每个结点是用来维护一段区间的最大值或总和;
而权值线段树的每个结点储存的一段区间有多少个数;
权值线段树主要用来求区间第
题目传送门
在
两重循环枚举,第一层枚举中心点
#include<bits/stdc++.h>
#define int long long
using namespace std;
int a[100005];
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
int ans=0;
for(int i=1;i<=n;i++)
{
int cnt1=0, cnt2=0;
for(int j=1;j<i;j++)
{
if(a[j]<a[i])
{
cnt1++;
}
}
for(int j=i+1;j<=n;j++)
{
if(a[i]<a[j])
{
cnt2++;
}
}
ans+=cnt1*cnt2;
}
cout<<ans;
return 1;
}
刚刚的暴力写法建立在题目不卡常的情况下,若题目卡常就寄了。这时我们可以用权值线段树优化时间复杂度。
从
注意:
要统计两次分别是
统计完第一次后要重新建树。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int a[N], n, m, tag[4*N], tree[4*N];
int cnt1[400005], cnt2[400005];
void pushup(int cur)
{
tree[cur]=tree[cur*2]+tree[cur*2+1];
return ;
}
void addtag(int cur, int lt, int rt, int val)
{
tag[cur]+=val;
tree[cur]+=(rt-lt+1)*val;
return ;
}
void pushdown(int cur, int lt, int rt)
{
if(tag[cur]==0)
{
return ;
}
int mid=lt+rt>>1;
addtag(cur*2,lt,mid,tag[cur]);
addtag(cur*2+1,mid+1,rt,tag[cur]);
tag[cur]=0;
return ;
}
void build(int cur, int lt, int rt)
{
if(lt==rt)
{
tree[cur]=0;
return ;
}
int mid=lt+rt>>1;
build(cur*2,lt,mid);
build(cur*2+1,mid+1,rt);
pushup(cur);
return ;
}
int query(int cur, int lt, int rt, int qx, int qy)
{
if(qy<lt||qx>rt)
{
return 0;
}
if(qx<=lt&&rt<=qy)
{
return tree[cur];
}
pushdown(cur,lt,rt);
int mid=lt+rt>>1;
return query(cur*2,lt,mid,qx,qy)+query(cur*2+1,mid+1,rt,qx,qy);
}
void update(int cur, int lt, int rt, int qx, int qy, int val)
{
if(qy<lt||qx>rt)
{
return ;
}
if(qx<=lt&&rt<=qy)
{
addtag(cur,lt,rt,val);
return ;
}
pushdown(cur,lt,rt);
int mid=lt+rt>>1;
update(cur*2,lt,mid,qx,qy,val);
update(cur*2+1,mid+1,rt,qx,qy,val);
pushup(cur);
return ;
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
build(1,1,1e5);
for(int i=1;i<=n;i++)
{
cnt1[i]=query(1,1,1e5,1,a[i]-1);
update(1,1,1e5,a[i],a[i],1);
}
build(1,1,1e5);
int ans=0;
for(int i=n;i>=1;i--)
{
cnt2[i]=query(1,1,1e5,a[i]+1,1e5);
update(1,1,1e5,a[i],a[i],1);
ans+=cnt1[i]*cnt2[i];
}
cout<<ans;
return 0;
}
题目传送门
求
就不写暴力了,快速进入正题。
这题跟上题很相似,唯一不同的两点是本题建树只建一次和要从小到大排序。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e5+5;
struct node
{
int v, id;
}a[N];
int n, m, tag[4*N], tree[4*N];
int cnt1[400005], cnt2[400005];
bool cmp(node x, node y)
{
if(x.v==y.v)
{
return x.id<y.id;
}
return x.v<y.v;
}
void pushup(int cur)
{
tree[cur]=tree[cur*2]+tree[cur*2+1];
return ;
}
void addtag(int cur, int lt, int rt, int val)
{
tag[cur]+=val;
tree[cur]+=(rt-lt+1)*val;
return ;
}
void pushdown(int cur, int lt, int rt)
{
if(tag[cur]==0)
{
return ;
}
int mid=lt+rt>>1;
addtag(cur*2,lt,mid,tag[cur]);
addtag(cur*2+1,mid+1,rt,tag[cur]);
tag[cur]=0;
return ;
}
void build(int cur, int lt, int rt)
{
if(lt==rt)
{
tree[cur]=0;
return ;
}
int mid=lt+rt>>1;
build(cur*2,lt,mid);
build(cur*2+1,mid+1,rt);
pushup(cur);
return ;
}
int query(int cur, int lt, int rt, int qx, int qy)
{
if(qy<lt||qx>rt)
{
return 0;
}
if(qx<=lt&&rt<=qy)
{
return tree[cur];
}
pushdown(cur,lt,rt);
int mid=lt+rt>>1;
return query(cur*2,lt,mid,qx,qy)+query(cur*2+1,mid+1,rt,qx,qy);
}
void update(int cur, int lt, int rt, int qx, int qy, int val)
{
if(qy<lt||qx>rt)
{
return ;
}
if(qx<=lt&&rt<=qy)
{
addtag(cur,lt,rt,val);
return ;
}
pushdown(cur,lt,rt);
int mid=lt+rt>>1;
update(cur*2,lt,mid,qx,qy,val);
update(cur*2+1,mid+1,rt,qx,qy,val);
pushup(cur);
return ;
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i].v;
a[i].id=i;
}
sort(a+1,a+1+n,cmp);
build(1,1,5e5);
int ans=0;
for(int i=1;i<=n;i++)
{
ans+=query(1,1,5e5,a[i].id+1,5e5);
update(1,1,5e5,a[i].id,a[i].id,1);
}
cout<<ans;
return 0;
}
树形 DP,顾名思义,在树形结构上的 DP 问题。
按照题目可将树形 DP 分为两类:
第一类:兄弟子树之间无数量依赖关系的 DP。
第二类:兄弟子树之间有数量依赖关系的 DP(实际上就是树上 背包)。
给定
这道题适合刚学的同学们当作模版题去理解。首先先从这道题需要用到的算法入手。由题面可知,本题中小的子树可以向大的子树转移,且无后效性,可以考虑树形 DP。
DP 四步法——状态、答案、状态转移方程和初始状态。
void tarjan(int cur)
{
stk.push(cur);
viss[cur]=1;
low[cur]=dfn[cur]=++cnt;
for(int nxt:nbr[cur])
{
if(!dfn[nxt])
{
tarjan(nxt);
low[cur]=min(low[cur],low[nxt]);
}
else if(viss[nxt])
{
low[cur]=min(low[cur],dfn[nxt]);
}
}
if(dfn[cur]==low[cur])
{
sum++;
while(stk.top()!=cur)
{
int tmp=stk.top();
stk.pop();
scc[tmp]=sum;
ans[sum].push_back(tmp);
viss[tmp]=0;
}
stk.pop();
ans[sum].push_back(cur);
scc[cur]=sum;
viss[cur]=0;
}
}
Kosaraju 算法由 S. Rao Kosaraju 在 1978 年一篇未发表的论文中提出,但 Micha Sharir 最早发表了它。
创建反图。
两遍 dfs。
第一次 dfs,选取任意顶点作为起点,遍历所有未访问过的顶点,并在回溯之前给顶点编号,也就是后序遍历。
第二次 dfs,对于反向后的图,以标号最大的顶点作为起点开始 DFS。这样遍历到的顶点集合就是一个强连通分量。对于所有未访问过的结点,选取标号最大的,重复上述过程。
两次 dfs 结束后,强连通分量就找出来了,Kosaraju 算法的时间复杂度为
由于 Kosaraju 算法应用面没有 Tarjan 算法广,故在此几笔带过。
void dfs1(int u) {
vis[u] = true;
for (int v : g[u])
if (!vis[v]) dfs1(v);
s.push_back(u);
}
void dfs2(int u) {
color[u] = sccCnt;
for (int v : g2[u])
if (!color[v]) dfs2(v);
}
void kosaraju() {
sccCnt = 0;
for (int i = 1; i <= n; ++i)
if (!vis[i]) dfs1(i);
for (int i = n; i >= 1; --i)
if (!color[s[i]]) {
++sccCnt;
dfs2(s[i]);
}
}
其中
Garbow 算法是 Tarjan 算法的另一种实现(注意,并不是优化),Tarjan 算法是用
从节点
在这个过程中每一个被弹出的节点都属于同一个 SCC。
当回溯到某一个节点
Garbow 算法的时间复杂度为
由于 Tarjan 算法的广泛出现,Garbow 算法没有人会系统的学习,故在此几笔带过。
int garbow(int u) {
stack1[++p1] = u;
stack2[++p2] = u;
low[u] = ++dfs_clock;
for (int i = head[u]; i; i = e[i].next) {
int v = e[i].to;
if (!low[v])
garbow(v);
else if (!sccno[v])
while (low[stack2[p2]] > low[v]) p2--;
}
if (stack2[p2] == u) {
p2--;
scc_cnt++;
do {
sccno[stack1[p1]] = scc_cnt;
// all_scc[scc_cnt] ++;
} while (stack1[p1--] != u);
}
return 0;
}
void find_scc(int n) {
dfs_clock = scc_cnt = 0;
p1 = p2 = 0;
memset(sccno, 0, sizeof(sccno));
memset(low, 0, sizeof(low));
for (int i = 1; i <= n; i++)
if (!low[i]) garbow(i);
}
给定一张
注意,本题可能存在重边和自环。
第一行两个正整数
接下来
第一行一个整数表示这张图的强连通分量数目。
接下来每行输出一个强连通分量。第一行输出 1 号点所在强连通分量,第二行输出 2 号点所在强连通分量,若已被输出,则改为输出 3 号点所在强连通分量,以此类推。每个强连通分量按节点编号大小输出。
6 8
1 2
1 5
2 6
5 6
6 1
5 3
6 4
3 4
3
1 2 5 6
3
4
对于所有数据,
本题主要难点在第二问“接下来每行输出一个强连通分量。第一行输出 1 号点所在强连通分量,第二行输出 2 号点所在强连通分量,若已被输出,则改为输出 3 号点所在强连通分量,以此类推。每个强连通分量按节点编号大小输出。”上。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int low[N], dfn[N], cnt, sum, scc[N], n, m;
bool viss[N];
stack<int> stk;
vector<int> nbr[N], ans[N];
void tarjan(int cur)//找 SCC
{
stk.push(cur);
viss[cur]=1;
low[cur]=dfn[cur]=++cnt;
for(int nxt:nbr[cur])
{
if(!dfn[nxt])
{
tarjan(nxt);
low[cur]=min(low[cur],low[nxt]);
}
else if(viss[nxt])
{
low[cur]=min(low[cur],dfn[nxt]);
}
}
if(dfn[cur]==low[cur])
{
sum++;
while(stk.top()!=cur)
{
int tmp=stk.top();
stk.pop();
scc[tmp]=sum;
ans[sum].push_back(tmp);
viss[tmp]=0;
}
stk.pop();
ans[sum].push_back(cur);
scc[cur]=sum;
viss[cur]=0;
}
}
signed main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u, v;
cin>>u>>v;
nbr[u].push_back(v);
}
for(int i=1;i<=n;i++)
{
if(!dfn[i])
{
tarjan(i);
}
}
cout<<sum<<"\n";
for(int i=1;i<=n;i++)
{
if(dfn[i]==0)
{
continue;
}
sort(ans[scc[i]].begin(), ans[scc[i]].end());//vector 从小到大排序
for(int cur:ans[scc[i]])
{
cout<<cur<<" ";
dfn[cur]=0;
}
cout<<"\n";
}
}
动态数组
P1455 搭配购买
P3916 图的遍历
先 Tarjan 缩点。
在同一个 SCC 中的点可以相互到达,那么我们可以在缩点时记录每个 SCC 中编号最大的点。
一个 SCC 中所有点能到达的最大编号的点即为这个 SCC 能到达的最大的编号。
缩点代码就不放了,在这里提供普通 dfs 的代码。
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n, m, ans[1000005];
vector<int> nbr[100005];
void dfs(int cur, int maxi)
{
if(ans[cur]!=0)
{
return ;
}
ans[cur]=maxi;
for(int nxt:nbr[cur])
{
dfs(nxt,maxi);
}
}
signed main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int x, y;
cin>>x>>y;
nbr[y].push_back(x);
}
for(int i=n;i>=1;i--)
{
if(ans[i]==0)
{
dfs(i,i);
}
}
for(int i=1;i<=n;i++)
{
cout<<ans[i]<<" ";
}
}
P3387 【模板】缩点
P2863 USACO06JAN The Cow Prom S
P2341 USACO03FALL / HAOI2006 受欢迎的牛 G
P2002 消息扩散
P1073 NOIP2009 提高组 最优贸易
先来看一张图
在这个图中,有
将这
其中
点
点
点
点
点
这就是所谓的缩点,缩成的点的点权为原 SCC 内点权之和。这也是本题的重要思想。
为什么要缩点呢?在本题中,点权最大的路径可以使用拓扑排序+dp,但拓扑排序的前提是有向无环图(DAG),而本题明显有环。缩点就让环变成一个点了。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+5;
vector<int>nbr[maxn];
stack<int>stk;
int in[maxn];
int a[maxn], sum[maxn];
int dp[maxn];
int n, m, dfn[maxn], used[maxn], vis[maxn], low[maxn], scc[maxn], num[maxn], cntc, cnt, ans;
struct node
{
int x, y;
}e[maxn];
void tarjan(int cur)
{
dfn[cur]=low[cur]=++cnt;
stk.push(cur);
for(int q:nbr[cur])
{
if(!dfn[q])
{
tarjan(q);
low[cur]=min(low[cur],low[q]);
}
else if(!scc[q])
{
low[cur]=min(low[cur],dfn[q]);
}
}
if(low[cur]==dfn[cur])
{
cntc++;
scc[cur]=cntc;
num[cntc]++;
while(stk.top()!=cur)
{
int t=stk.top();
stk.pop();
scc[t]=cntc;
num[cntc]++;
}
stk.pop();
}
}
void topo()
{
queue<int> q;
for(int i=1;i<=cntc;i++)
{
if(in[i]==0)
{
q.push(i);
dp[i]=sum[i];
}
}
while(q.empty()==0)
{
int cur=q.front();
q.pop();
for(int nxt:nbr[cur])
{
in[nxt]--;
dp[nxt]=max(dp[nxt],dp[cur]+sum[nxt]);
if(in[nxt]==0)
{
q.push(nxt);
}
}
}
int maxi=-1e9;
for(int i=1;i<=cntc;i++)
{
maxi=max(maxi,dp[i]);
}
cout<<maxi;
return ;
}
signed main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
for(int i=1;i<=m;i++)
{
cin>>e[i].x>>e[i].y;
nbr[e[i].x].push_back(e[i].y);
}
for(int i=1;i<=n;i++)
{
if(!dfn[i])
{
tarjan(i);
}
}
for(int i=1;i<=n;i++)
{
sum[scc[i]]+=a[i];
}
for(int i=1;i<=10004;i++)
{
nbr[i].clear();
}
for(int i=1;i<=m;i++)
{
if(scc[e[i].x]!=scc[e[i].y])
{
nbr[scc[e[i].x]].push_back(scc[e[i].y]);
in[scc[e[i].y]]++;
}
}
topo();
return 0;
}
记录每个 SCC 的大小,最后判断是不是大于
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e4+5;
vector<int>nbr[maxn];
stack<int>s;
int n, m, dfn[maxn], used[maxn], vis[maxn], low[maxn], color[maxn], num[maxn], cntc, cnt, ans;
void add(int x)
{
s.pop();
color[x]=cntc;
num[cntc]++;
vis[x]=false;
}
void tarjan(int x)
{
dfn[x]=low[x]=++cnt;
s.push(x);
vis[x]=used[x]=true;
for(int q:nbr[x])
{
if(!dfn[q])
{
tarjan(q);
low[x]=min(low[x],low[q]);
}
else if(vis[q])
{
low[x]=min(low[x],dfn[q]);
}
}
if(low[x]==dfn[x])
{
cntc++;
while(s.top()!=x)
{
int t=s.top();
add(t);
}
add(x);
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u,v;
cin>>u>>v;
nbr[u].push_back(v);
}
for(int i=1;i<=n;i++)
{
if(!used[i])
{
tarjan(i);
}
}
for(int i=1;i<=cntc;i++)
{
if (num[i]>1)
{
ans++;
}
}
cout<<ans;
return 0;
}
Tarjan
由题可得,受欢迎的奶牛只有可能是图中唯一的入度为零的强连通分量中的所有奶牛,所以若出现两个以上入度为
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
vector<int>nbr[maxn];
stack<int>s;
int ans1=0;
int in[maxn];
int a[maxn];
int n, m, dfn[maxn], used[maxn], vis[maxn], low[maxn], color[maxn], num[maxn], cntc, cnt, ans;
void add(int x)
{
color[x]=cntc;
num[cntc]++;
}
void tarjan(int x)
{
dfn[x]=low[x]=++cnt;
s.push(x);
for(int q:nbr[x])
{
if(!dfn[q])
{
tarjan(q);
low[x]=min(low[x],low[q]);
}
else if(!color[q])
{
low[x]=min(low[x],dfn[q]);
}
}
if(low[x]==dfn[x])
{
cntc++;
add(x);
while(s.top()!=x)
{
int t=s.top();
add(t);
s.pop();
}
s.pop();
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u,v;
cin>>u>>v;
nbr[v].push_back(u);
}
for(int i=1;i<=n;i++)
{
if(!dfn[i])
{
tarjan(i);
}
}
for(int i=1;i<=n;i++)
{
for(int nxt:nbr[i])
{
if(color[i]!=color[nxt])
{
in[color[nxt]]++;
}
}
}
int ans=0, cntu=0;
for(int i=1;i<=cntc;i++)
{
if(!in[i])
{
ans=num[i];
cntu++;
}
}
if(cntu==1)cout<<ans;
else cout<<0;
return 0;
}
放几道题在这,难度很大,诸位加油~。
P1262 间谍网络
P3627 APIO2009 抢掠计划
UVA11324 The Largest Clique
P2746 USACO5.3 校园网Network of Schools
P2812 校园网络【USACO Network of Schools加强版】
P1407 国家集训队 稳定婚姻
P2169 正则表达式
P4819 中山市选 sr游戏
P2403 SDOI2010 所驼门王的宝藏
P4339 ZJOI2018 迷宫
P4233 射命丸文的笔记
CF949C
P3275 [SCOI2011] 糖果
在给定的
答案:
初始状态:
若有环,则先缩点,若环上有边权为
目标 1:求最少发几个软件,使得
目标 2:最少连几条边,使得无论发
跑 Tarjan 缩点,建新图,统计入度。
入度为
入度为
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n, m, a[N], num[N], low[N], dfn[N], in[N], sum[N], cnt, cntc, scc[N];
bool viss[N];
struct node
{
int x, y;
}e[N<<2];
queue<int> q;
int dp[N], out[N];
vector<int> nbr[N];
stack<int> stk;
void tarjan(int cur)
{
low[cur]=dfn[cur]=++cnt;
viss[cur]=1;
stk.push(cur);
for(int nxt:nbr[cur])
{
if(!dfn[nxt])
{
tarjan(nxt);
low[cur]=min(low[cur],low[nxt]);
}
else if(viss[nxt])
{
low[cur]=min(low[cur],dfn[nxt]);
}
}
if(dfn[cur]==low[cur])
{
cntc++;
scc[cur]=cntc;
num[cntc]++;
viss[cur]=0;
while(stk.top()!=cur)
{
int nxt=stk.top();
stk.pop();
num[cntc]++;
viss[nxt]=0;
scc[nxt]=cntc;
}
stk.pop();
}
}
int cntd=0;
signed main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
int x;
while(cin>>x)
{
if(x==0)
{
break;
}
nbr[i].push_back(x);
cntd++;
e[cntd].x=i;
e[cntd].y=x;
}
}
for(int i=1;i<=n;i++)
{
if(!dfn[i])
{
tarjan(i);
}
}
int ind=0, outd=0;
for(int i=1;i<=cntd;i++)
{
if(scc[e[i].x]!=scc[e[i].y])
{
in[scc[e[i].y]]++;
out[scc[e[i].x]]++;
}
}
for(int i=1;i<=cntc;i++)
{
ind+=!in[i];
outd+=!out[i];
}
if(cntc==1)
{
cout<<ind<<"\n"<<0;
}
else
{
cout<<ind<<"\n"<<max(ind,outd);
}
}
是指队列维护的元素单调、下标也单调的数据结构。
单调队列不像优先队列,是一种C++自带的STL,单调队列是用普通的队列进行维护的。
滑动窗口。
在一个固定大小的窗口内寻找最值,且窗口从左到右移动。
暴力:
线段树:
RMQ:
单调队列:
显然,用单调队列维护时间复杂度最优。
循环枚举下标
检查队头是否过期,并从队头删除过期下标。
输出队头元素,即为当前窗口最大值(具体看题目)。
时间复杂度分析:每个元素只出队入队一次,总时间复杂度为
单调队列模板题。
系列难度:P2032
放个P1886的代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5;
int a[N], q[N], n, k, head=1, tail;
void getmin()
{
head=1, tail=0;
for(int i=1;i<=n;i++)
{
while(head<=tail&&a[q[tail]]>=a[i])
{
tail--;
}
q[++tail]=i;
while(head<=tail&&q[tail]-q[head]+1>k)
{
head++;
}
if(i>=k)cout<<a[q[head]]<<" ";
}
cout<<"\n";
}
void getmax()
{
memset(q,0,sizeof(q));
head=1, tail=0;
for(int i=1;i<=n;i++)
{
while(head<=tail&&a[q[tail]]<=a[i])
{
tail--;
}
q[++tail]=i;
while(head<=tail&&q[tail]-q[head]+1>k)
{
head++;
}
if(i>=k)cout<<a[q[head]]<<" ";
}
cout<<"\n";
}
signed main()
{
cin>>n>>k;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
getmin();
getmax();
}
稍微包装了一下的模板题。
系列难度:P1638
单调队列中套了一个简单算法,这次套的前缀和。
难度上升了不止一点。
P2698在二次单调队列中套了个二分。
难度都差不多,P2564有一些坑点,P2698难写一些。
P2698也有坑点,和单调队列窗口大小有关。
单调队列虽然是线段树等数据结构中难度中下级的数据结构,但与其他算法结合起来难度还是不小的。
讲个难点的,P2698
由于每滴水以
假设花盆宽度
维护
枚举花盆宽度可行但会TLE,宽度有单调性,一眼二分。
注意事项:
枚举将水滴按
花盆宽度为
贴个 check 函数
bool check(int x)
{
head=1,tail=0,head1=1,tail1=0;
for(int i=1;i<=n;i++)
{
while(head<=tail&&a[i].y>=a[q[tail]].y)
{
tail--;
}
q[++tail]=i;
while(tail-head>=0&&a[q[tail]].x-a[q[head]].x+1>x)
{
head++;
}
while(head1<=tail1&&a[i].y<=a[qq[tail1]].y)
{
tail1--;
}
qq[++tail1]=i;
while(tail1-head1>=0&&a[qq[tail1]].x-a[qq[head1]].x+1>x)
{
head1++;
}
if(abs(a[q[head]].y-a[qq[head1]].y)>=m)
{
return 1;
}
}
return 0;
}
一种下标单调、元素也单调的栈。
单调栈同单调队列不是一种C++自带的STL,单调栈是用普通的栈进行维护的。
在若干区间内找最值,转化为枚举每个最值找区间。
寻找每个元素
枚举
循环结束后,剩余栈中元素下标说明其右侧没有更大的元素。
同上,倒序枚举
P5788 模板题
P2947 跟模板题差不多,套了个背景。
P2866 初学者建议去做,码量短,有少许思维难度。
CF547B 做法多样,难度中等偏上少许,有一定思维难度,记得看讨论区的翻译。
CF1299C 这题建议已开始学CSP-S相关内容的同学做,难度思维码量都有的。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e6+5;
int n, a[N], ans[N];
stack<int> stk;
signed main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
a[n+1]=1e9;
}
for(int i=1;i<=n;i++)
{
while(!stk.empty()&&a[i]>a[stk.top()])
{
ans[stk.top()]=i;
stk.pop();
}
stk.push(i);
}
for(int i=1;i<=n;i++)
{
cout<<ans[i]<<" ";
}
return 0;
}
以点为研究对象的贪心策略。
vis[cur]=1
。 BFS 是边权为
#define int long long
。多次调用 dijkstra 要重置 vis[]
和 dis[]
。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+5, M=1e5+5;
int dis[N], n, m, s; bool vis[N];
struct node
{
int y, val;
};
vector<node> nbr[N];
void dijkstra()
{
for(int i=1;i<=n;i++)
{
dis[i]=2147483647;
}
dis[s]=0;
for(int i=1;i<=n;i++)
{
int mini=1e18, id;
for(int j=1;j<=n;j++)
{
if(mini>dis[j]&&!vis[j])
{
id=j;
mini=dis[j];
}
}
vis[id]=1;
for(auto nxt:nbr[id])
{
int y=nxt.y, val=nxt.val;
if(dis[id]+val<dis[y])
{
dis[y]=dis[id]+val;
}
}
}
}
signed main()
{
cin>>n>>m>>s;
for(int i=1;i<=m;i++)
{
int x, y, w;
cin>>x>>y>>w;
nbr[x].push_back({y,w});
}
dis[s]=0;
dijkstra();
for(int i=1;i<=n;i++)
{
cout<<dis[i]<<" ";
}
}
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+5;
int n,m,dis[N],s;
bool vis[N];
struct node
{
int y,w;
};
vector<node> nbr[N];
void spfa()
{
for(int i=1;i<=n;i++)
{
dis[i]=2147483647;
}
dis[s]=0;
queue<int> q;
q.push(s);
vis[s]=true;
while(q.empty()==false)
{
int cur=q.front();
q.pop();
vis[cur]=false;
for(int i=0;i<nbr[cur].size();i++)
{
int nxt=nbr[cur][i].y,w=nbr[cur][i].w;
if(dis[cur]+w<dis[nxt])
{
dis[nxt]=dis[cur]+w;
if(vis[nxt]==false)
{
vis[nxt]=true;
q.push(nxt);
}
}
}
}
}
signed main()
{
cin>>n>>m>>s;
for(int i=1;i<=m;i++)
{
int u,v,w;
cin>>u>>v>>w;
nbr[u].push_back((node){v,w});
}
spfa();
for(int i=1;i<=n;i++)
cout<<dis[i]<<" ";
return 0;
}
有一个
7 11 5 4 2 4 2 1 4 3 7 2 2 3 4 3 5 7 5 7 3 3 6 1 1 6 3 4 2 4 3 5 6 3 7 2 1
7
就一模板。
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n, m, dis[2505][2505], t, s;
signed main()
{
cin>>n>>m>>s>>t;
for(int i=1;i<=2500;i++)
{
for(int j=1;j<=2500;j++)
{
dis[i][j]=1e9;
}
}
for(int i=1;i<=m;i++)
{
int x, y, w;
cin>>x>>y>>w;
dis[x][y]=dis[y][x]=w;
}
for(int i=1;i<=n;i++)
{
dis[i][i]=0;
}
for(int k=1;k<=n;k++)
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(dis[i][k]==1000000000||dis[k][j]==1000000000)
{
continue;
}
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
}
}
}
cout<<dis[s][t];
}
也就一模板,注意是无向图。
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n, m, s, t;
struct node
{
int y, val;
};
vector<node> nbr[2505];
int dis[2505];
bool vis[2505];
struct Node
{
int y, val;
bool operator<(const Node &b)const
{
return val>b.val;
}
};
void dijkstra()
{
memset(vis,0,sizeof vis);
priority_queue<Node> q;
for(int i=1;i<=n;i++)
{
dis[i]=2147483647;
}
q.push((Node){s,0});
dis[s]=0;
while(!q.empty())
{
Node now=q.top();
q.pop();
int cur=now.y;
if(vis[cur])
{
continue;
}
vis[cur]=1;
for(auto nxt:nbr[cur])
{
int y=nxt.y, val=nxt.val;
if(dis[cur]+val<dis[y])
{
dis[y]=dis[cur]+val;
q.push((Node){y,dis[y]});
}
}
}
}
signed main()
{
cin>>n>>m>>s>>t;
for(int i=1;i<=m;i++)
{
int x, y, w;
cin>>x>>y>>w;
nbr[x].push_back((node){y,w});
nbr[y].push_back((node){x,w});
}
dis[s]=0;
dijkstra();
cout<<dis[t];
}
有一个邮递员要送东西,邮局在节点
5 10 2 3 5 1 5 5 3 5 6 1 2 8 1 3 8 5 3 4 4 1 8 4 5 3 3 5 6 5 4 2
83
很明显,求起点到各个点的最短路,一遍是求起点到各个点最短路和,另一遍求各个点返回起点最短路和,跑两遍 Dijkstra。 建反图,两次 Dijkstra 之间要清空。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+5, M=1e5+5;
int dis[N], n, m, s;
bool vis[N];
struct node
{
int y, val;
};
struct Node
{
int y, val;
bool operator<(const Node &b)const
{
return val>b.val;
}
};
vector<node> nbr[N];
void dijkstra()
{
memset(vis,0,sizeof vis);
priority_queue<Node> q;
for(int i=1;i<=n*2;i++)
{
dis[i]=2147483647;
}
q.push((Node){s,0});
dis[s]=0;
while(!q.empty())
{
Node now=q.top();
q.pop();
int cur=now.y;
if(vis[cur])
{
continue;
}
vis[cur]=1;
for(auto nxt:nbr[cur])
{
int y=nxt.y, val=nxt.val;
if(dis[cur]+val<dis[y])
{
dis[y]=dis[cur]+val;
q.push((Node){y,dis[y]});
}
}
}
}
int t;
signed main(void)
{
int cnt=0;
cin>>n>>m;
s=1;
for(int i=1;i<=m;i++)
{
int x, y, w;
cin>>x>>y>>w;
nbr[x].push_back({y,w});
nbr[y+n].push_back({x+n,w});
}
dis[s]=0;
dijkstra();
for(int i=2;i<=n;i++)
{
cnt+=dis[i];
}
s=n+1;
dis[s]=0;
dijkstra();
for(int i=2+n;i<=n+n;i++)
{
cnt+=dis[i];
}
cout<<cnt;
}
memset(vis,0,sizeof vis);
dis[s]=100.0;
q.push(s);
vis[s]=1;
while(!q.empty())
{
int cur=q.front();
q.pop();
vis[cur]=0;
for(auto qq:nbr[cur])
{
int nxt=qq.y;
double w=qq.w;
if(dis[cur]/w<dis[nxt])
{
dis[nxt]=dis[cur]/w;
if(!vis[nxt])
{
vis[nxt]=1;
q.push(nxt);
}
}
}
}
Bellman-Ford 算法是由理查德·贝尔曼(Richard Bellman) 和 莱斯特·福特 创立的,求解单源最短路径问题的一种算法。
有时候这种算法也被称为 Moore-Bellman-Ford 算法,因为 Edward F. Moore 也为这个算法的发展做出了贡献。
它的原理是对图进行
其优于 Dijkstra 算法的方面是边的权值可以为负数、实现简单,缺点是时间复杂度过高,高达
双重循环枚举,和暴力差不多。
#include<bits/stdc++.h>
#define int long long
using namespace std;
struct node
{
int x, y, w;
}e[2000005];
int n, m, s;
int dis[200005];
void bellman_ford()
{
for(int i=1;i<=n;i++)
{
dis[i]=2147483647;
}
dis[s]=0;
for(int t=1;t<n;t++)
{
for(int i=1;i<=m;i++)
{
if(dis[e[i].x]+e[i].w<dis[e[i].y])
{
dis[e[i].y]=dis[e[i].x]+e[i].w;
}
}
}
}
signed main()
{
cin>>n>>m>>s;
for(int i=1;i<=m;i++)
{
cin>>e[i].x>>e[i].y>>e[i].w;
}
bellman_ford();
for(int i=1;i<=n;i++)
{
cout<<dis[i]<<" ";
}
}
死了的算法。
理论上对 Bellman-Ford 算法进行优化,但这种优化被证实可卡掉的。
用队列进行松弛。
除了队列优化(SPFA)之外,Bellman–Ford 还有其他形式的优化,这些优化在部分图上效果明显,但在某些特殊图上,最坏复杂度可能达到指数级。
上述“其他优化”来自 OI-WIKI
但一句话总结,都会被卡。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+5;
int t, n, m;
struct node
{
int y, w;
};
vector<node> nbr[N];
int cnt[N], dis[N];
bool vis[N];
void spfa()
{
memset(cnt,0,sizeof cnt);
memset(vis,0,sizeof vis);
memset(dis,0x3f,sizeof dis);
queue<int> q;
q.push(1);
vis[1]=1;
while(!q.empty())
{
int cur=q.front();
q.pop();
vis[cur]=0;
for(auto qq:nbr[cur])
{
int nxt=qq.y, val=qq.w;
if(dis[cur]+val<dis[nxt])
{
dis[nxt]=dis[cur]+val;
cnt[nxt]=cnt[cur]+1;
if(cnt[nxt]>n-1)
{
cout<<"YES\n";
return ;
}
q.push(nxt);
}
}
}
cout<<"NO\n";
return ;
}
signed main()
{
cin>>t;
while(t--)
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
nbr[i].clear();
}
for(int i=1;i<=m;i++)
{
int x, y, w;
cin>>x>>y>>w;
if(w>=0)
{
nbr[y].push_back((node){x,w});
}
nbr[x].push_back((node){y,w});
}
spfa();
}
}
我们用队列来维护哪些结点可能会引起松弛操作,就能避免访问不必要的边。
这个算法已经死了就不多赘述了。
当不同结点作为根结点时,状态转移的结果不一样,若枚举每个点作为根结点再DP,时间复杂度会很高。
这种情况下,使用换根DP处理相邻两个结点之间的贡献,达到快速换根的效果。
对于一棵树,寻找以某个点
任选
从根结点(上述中我们用
很板的一道换根 DP 题。
定义
初始状态:
定义
考虑从上往下的转移,如从
虽然这题可以省略,但为了养成好习惯,
记得开 long long
。
对于一个序列,以区间为子问题的一类DP问题
初始状态:
发现字符串总由短拓展到长,考虑区间 DP。
定义状态
答案:if(dp[1][n][0])cout<<'W';.....
。
(其他字符的情况以此类推)
状态转移是 ex 小分讨就不放代码了(怕有人贺)。
P8675 1.积木不能悬空,同一行不能间断,考虑从下往上递推;
2.同一行必须连续,考虑枚举区间起点和终点;
3.状态:
4.答案: (sigma(dp[L][i][j])+ 1) % mod;
5.转移:
for(int i= 1;i <= m; i++)
for(int j=i; j <= m; j++)
for(int ii = 1; ii <= i; ii++)
for(int jj=j; jj<=m;jj++)
if(vis[L][j] - vis[L][i-1]== 0)//i到i没有障碍物
dp[L][i][j]=(dp[L][i][j] + dp[L-1][ii][jj]) % mod;
初始: dp[0][1][m]=1
7.时间 O(n^5)
8.利用二维前缀和sum[i][j]维护以(i,j)端点时dp[][]的区间和,加速枚举
是一种分治思想,通常指的是序列分块。对于区间修改和区间询问总是分成左端不完整的块、中间完整的块、右端不完整的块三个部分处理
使用范围更广泛,但时间复杂度更高。
维护一个区间标记
分情况讨论:
情况1:询问区间 [L,R] 在一个块中,暴力枚举
情况2:区间 [L,R] 不在同一块中,分为三个部分:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int a[N];
int L[N], R[N], pos[N], n, m, t, tag[N], sum[N];
void update(int lt, int rt, int val)
{
int x=pos[lt], y=pos[rt];
if(x==y)
{
for(int i=lt;i<=rt;i++)
{
a[i]+=val;
sum[x]+=val;
}
}
else
{
for(int i=x+1;i<=y-1;i++)
{
tag[i]+=val;
}
for(int i=lt;i<=R[x];i++)
{
a[i]+=val, sum[x]+=val;
}
for(int i=L[y];i<=rt;i++)
{
a[i]+=val;
sum[y]+=val;
}
}
return ;
}
int query(int lt, int rt)
{
int x=pos[lt], y=pos[rt];
int ans=0;
if(x==y)
{
for(int i=lt;i<=rt;i++)
{
ans+=a[i];
}
}
else
{
for(int i=x+1;i<=y-1;i++)
{
ans+=sum[i]+tag[i]*(R[i]-L[i]+1);
}
for(int i=lt;i<=R[x];i++)
{
ans+=a[i]+tag[x];
}
for(int i=L[y];i<=rt;i++)
{
ans+=a[i]+tag[y];
}
}
return ans;
}
signed main()
{
cin>>m;
cin>>n;
for(int i=1;i<=m;i++)
{
cin>>a[i];
}
t=sqrt(m);
for(int i=1;i<=t;i++)
{
L[i]=(i-1)*t+1;
R[i]=i*t;
}
if(R[t]<m)
{
t++;
L[t]=R[t-1]+1;
R[t]=m;
}
for(int i=1;i<=t;i++)
{
for(int j=L[i];j<=R[i];j++)
{
pos[j]=i;
sum[i]+=a[j];
}
}
for(int i=1;i<=n;i++)
{
int opt;
cin>>opt;
if(opt==1)
{
int x, y, k;
cin>>x>>y>>k;
update(x,y,k);
}
else
{
int x, y;
cin>>x>>y;
cout<<query(x,y)<<"\n";
}
}
}
线段树2就不放了。
询问:单独一块时暴力。否则不完整的块暴力,完整的块二分。
每次需要高频率还原
还原函数:
void resort(int x)
{
num[x].clear();
for(int i=L[x];i<=R[x];i++)
{
num[x].push_back(a[i]);
}
sort(num[x].begin(), num[x].end());
}
指的是在一个序列中多次进行静态区间求最值。
倍增思想。
分为两部分:预处理和询问(S 组算法里最长的预处理)。
维护
状态转移:
初始状态:
维护
对于
求覆盖区间一半以上的幂,
现在来考虑
考虑优先队列维护前
维护
当
否则,
对于以
循环从优先队列中取
是一种对状态表示形式的一种优化。
任何二进制数位;
&1
得到它本身。^1
则取反。&0
则赋值为 |1
则赋值为 通常二进制的第一位我们称之为第零位。
(n>>k)&1
取出二进制下 n&((1<<k)-1)
去除二进制下 n^(1<<k)
将二进制下 n|(1<<k)
将二进制下 n&(~(1<<k))
将二进制下 见 https://www.luogu.com.cn/article/z883dedy。
四则运算符
定义状态
答案为所有
维护
上下中草的两个状态按位与的结果为
种草状态
初始状态:
状态转移除了判肥沃以外还是有点板的。
定义状态为
答案即为
状态转移稍有点ex。
提示:需要维护一个 soil[i]=(soil[i]<<1)+(c=='P');
。
初始状态:
优化:滚动数组、提前筛可能状态。
for(int j=i;j!=0;j=(j-1)&i;
在树上取两点
本章主要讲的是倍增求 LCA。
从任意点对
若预处理任意点
定义状态
状态转移方程:枚举
初始状态:
void pre_lca(int cur, int fa)
{
dep[cur]=dep[fa]+1;
dp[cur][0]=fa;
for(int i=1;(1<<i)<=dep[cur];i++)
{
dp[cur][i]=dp[dp[cur][i-1]][i-1];
}
for(int nxt:nbr[cur])
{
if(nxt!=fa)pre_lca(nxt,cur);
}
}
第一步:约定深度较大的点,若
第二步:将深度较大的结点
第三步:判断
第四步:
第五步:
int lca(int x, int y)
{
if(dep[y]<dep[x])swap(x,y);
for(int i=20;i>-1;i--)
{
if(dep[dp[y][i]]>=dep[x])
{
y=dp[y][i];
}
}
if(x==y)return x;
for(int i=20;i>-1;i--)
{
if(dp[x][i]!=dp[y][i])
{
x=dp[x][i],y=dp[y][i];
}
}
return dp[x][0];
}
预处理是
#include<bits/stdc++.h>
#define int long long
using namespace std;
int dp[500005][21],dep[500005], n, m, s;
vector<int> nbr[500005];
void pre_lca(int cur, int fa)
{
dep[cur]=dep[fa]+1;
dp[cur][0]=fa;
for(int i=1;(1<<i)<=dep[cur];i++)
{
dp[cur][i]=dp[dp[cur][i-1]][i-1];
}
for(int nxt:nbr[cur])
{
if(nxt!=fa)pre_lca(nxt,cur);
}
}
int lca(int x, int y)
{
if(dep[y]<dep[x])swap(x,y);
for(int i=20;i>-1;i--)
{
if(dep[dp[y][i]]>=dep[x])
{
y=dp[y][i];
}
}
if(x==y)return x;
for(int i=20;i>-1;i--)
{
if(dp[x][i]!=dp[y][i])
{
x=dp[x][i],y=dp[y][i];
}
}
return dp[x][0];
}
signed main()
{
ios::sync_with_stdio(0),cin.tie(0);
cin>>n>>m>>s;
for(int i=1;i<n;i++)
{
int x, y;
cin>>x>>y;
nbr[x].push_back(y);
nbr[y].push_back(x);
}
pre_lca(s,0);
for(int i=1;i<=m;i++)
{
int x, y;
cin>>x>>y;
cout<<lca(x,y)<<"\n";
}
}
维护
$x$ 儿子结点中包含 $A$ 和 $B$ 的子树剔除,其余为答案。
中间位置的点
约定深度较大的点为
注意特殊情况:
从
void work(int x,int y)
{
if(x==y)
{
cout<<n<<"\n";
return ;
}
if(dep[x]==dep[y])
{
for(int i=14;i>=0;i--)
{
if(dp[x][i]!=dp[y][i])
{
x=dp[x][i],y=dp[y][i];
}
}
cout<<size[1]-size[x]-size[y]<<"\n";
return ;
}
if(dep[x]<dep[y]) swap(x,y);
if((dep[x]-dep[y])%2==1)
{
cout<<"0\n";
return ;
}
int x2=x,len=(dep[x]-dep[y])/2;
for(int i=14;i>=0;i--)
{
if(dep[dp[x][i]]>=dep[y])
{
x=dp[x][i];
}
}
if(x==y)
{
len+=dep[x];
for(int i=14;i>=0;i--)
{
if(dep[dp[x2][i]]>len)
{
x2=dp[x2][i];
}
}
cout<<size[dp[x2][0]]-size[x2]<<"\n";
return ;
}
for(int i=14;i>=0;i--)
{
if(dp[x][i]!=dp[y][i])
{
x=dp[x][i],y=dp[y][i];
}
}
len+=dep[x]-1;
for(int i=14;i>=0;i--)
{
if(dep[dp[x2][i]]>len)
{
x2=dp[x2][i];
}
}
cout<<size[dp[x2][0]]-size[x2]<<"\n";
}
见 https://www.luogu.com.cn/article/58jlmt7i
方便阅读搬过来。
从数据范围很容易发现,如果我们把边权累乘再判整数,炸掉是必然的,这时候,我们来发现一个性质:只有小数部分有
注意:若边权或点权为
用一个树上结点维护序列