前言
近来,去查漏补缺了一些知识,发现自己好像在暑假时候略微接触的点分治好像我还没好好的学过(当时教练说先学那些提高组要用到的东西)。所以呢,我就去网上找了几份讲义,整合了一下,那就在这篇博客里谈一谈我是怎么学习点分治的吧。
开始
其实呢,我学点分治一开始看了几份讲义,就以为自己懂了,然后呢,我做的第一道例题是“聪聪可可”。
聪聪可可题意:
给你一棵树,边带权,让你求树上点对之间距离是3的有序点对数?量,点的数量≤20000
链接:https://www.luogu.org/problemnew/show/2634
当时我一看觉得so easy,然后大力打了代码,一遍过样例,交上去就A了,当时发现我就相当于写了一个树形dp,但是呢,我用到了一个思想,那就是对于端点是自己子树上的任意一条路径,它要么经过自己,要么在自己的某个子树中。虽然感觉自己有点傻,但还是窃喜,毕竟用到了点分治的思想嘛。
应用
打完了第一题,我发现洛谷上好像有模板题,所以我就开始码模板题啦。
模板题题意:
给你n个点的树,边带权,有m次询问k,每一次都询问树上是否存在一条路径的长度为k,n≤10000,m≤100,K≤10000000
我打了一份代码,对于每一个算到的根节点,我都用容斥来计算,交了上去,发现就这么A了?不敢相信~
AC代码:
#include<cstdio>
#include<cstring>
#include<cctype>
namespace fast_IO
{
const int IN_LEN=10000000,OUT_LEN=10000000;
char ibuf[IN_LEN],obuf[OUT_LEN],*ih=ibuf+IN_LEN,*oh=obuf;
char *lastin=ibuf+IN_LEN;
const char *lastout=ibuf+OUT_LEN-1;
inline char getchar_()
{
if(ih==lastin)lastin=ibuf+fread(ibuf,1,IN_LEN,stdin),ih=ibuf;
return (*ih++);
}
inline void putchar_(const char x)
{
if(ih==lastout)fwrite(obuf,1,oh-obuf,stdout),oh=obuf;
*oh++=x;
}
inline void flush(){fwrite(obuf, 1, oh - obuf, stdout);}
}
using namespace fast_IO;
//#define getchar() getchar_()
//#define putchar(x) putchar_((x))
typedef long long LL;
template <typename T> inline T max(const T a,const T b){return a>b?a:b;}
template <typename T> inline T min(const T a,const T b){return a<b?a:b;}
template <typename T> inline T abs(const T a){return a>0?a:-a;}
template <typename T> inline void swap(T&a,T&b){T c=a;a=b;b=c;}
template <typename T> inline T gcd(const T a,const T b){if(a%b==0)return b;return gcd(b,a%b);}
template <typename T> inline void read(T&x)
{
char cu=getchar();x=0;bool fla=0;
while(!isdigit(cu)){if(cu=='-')fla=1;cu=getchar();}
while(isdigit(cu))x=x*10+cu-'0',cu=getchar();
if(fla)x=-x;
}
template <typename T> void printe(const T x)
{
if(x>=10)printe(x/10);
putchar(x%10+'0');
}
template <typename T> inline void print(const T x)
{
if(x<0)putchar('-'),printe(-x);
else printe(x);
}
const int maxn=10001,maxm=20001;
int n,m;
int head[maxn],tow[maxm],vau[maxm],nxt[maxm],tmp;
int Hash[maxn],dis[maxn];
int ans[10000001];
inline void addb(const int u,const int v,const int w)
{
tmp++;
nxt[tmp]=head[u];
head[u]=tmp;
tow[tmp]=v;
vau[tmp]=w;
}
int Q[maxn],tot;
void dfs(const int u,const int fa)
{
Q[++tot]=u;
for(register int i=head[u];~i;i=nxt[i])
{
const int v=tow[i];
if(v!=fa&&!Hash[v])
{
dis[v]=dis[u]+vau[i];
dfs(v,u);
}
}
}
void calc(const int u,const bool sign,const int more)
{
tot=0;
dis[u]=more;
dfs(u,u);
for(register int i=1;i<=tot;i++)
for(register int j=i+1;j<=tot;j++)
if(dis[Q[i]]+dis[Q[j]]<=10000000)
{
if(sign)ans[dis[Q[i]]+dis[Q[j]]]++;
else ans[dis[Q[i]]+dis[Q[j]]]--;
}
}
void solve(const int u)
{
calc(u,1,0);
Hash[u]=1;
for(register int i=head[u];~i;i=nxt[i])
{
const int v=tow[i];
if(!Hash[v])
{
calc(v,0,vau[i]);
solve(v);
}
}
}
int main()
{
memset(head,-1,sizeof(head));
read(n),read(m);
for(register int i=1;i<n;i++)
{
int u,v,w;read(u),read(v),read(w);
addb(u,v,w),addb(v,u,w);
}
solve(1);
for(register int i=1;i<=m;i++)
{
LL k;
read(k);
if(ans[k])puts("AYE");
else puts("NAY");
}
return flush(),0;
}
看起来是不是十分优越?
补充1
可是呢,这一题其实理论上如果出题人卡的话是可以卡掉的,为什么呢,因为考虑一条链的情况,复杂度是会要到n^3的(对于每一个节点子树大小的平方)
神奇的点分治自然有解决方法,点分治当中有一个操作,叫获取树的重心,贴一波代码,看那个getroot函数
#include<cstdio>
#include<cstring>
#include<cctype>
namespace fast_IO
{
const int IN_LEN=1000000,OUT_LEN=1000000;
char ibuf[IN_LEN],obuf[OUT_LEN],*ih=ibuf+IN_LEN,*oh=obuf,*lastin=ibuf+IN_LEN,*lastout=obuf+OUT_LEN-1;
inline char getchar_(){return (ih==lastin)&&(lastin=(ih=ibuf)+fread(ibuf,1,IN_LEN,stdin),ih==lastin)?EOF:*ih++;}
inline void putchar_(const char x){if(oh==lastout)fwrite(obuf,1,oh-obuf,stdout),oh=obuf;*oh++=x;}
inline void flush(){fwrite(obuf,1,oh-obuf,stdout);}
}
using namespace fast_IO;
#define getchar() getchar_()
#define putchar(x) putchar_((x))
typedef long long LL;
template <typename T> inline T max(const T a,const T b){return a>b?a:b;}
template <typename T> inline T min(const T a,const T b){return a<b?a:b;}
template <typename T> inline T abs(const T a){return a>0?a:-a;}
//template <typename T> inline void swap(T&a,T&b){T c=a;a=b;b=c;}
template <typename T> inline T gcd(const T a,const T b){if(a%b==0)return b;return gcd(b,a%b);}
template <typename T> inline void read(T&x)
{
char cu=getchar();x=0;bool fla=0;
while(!isdigit(cu)){if(cu=='-')fla=1;cu=getchar();}
while(isdigit(cu))x=x*10+cu-'0',cu=getchar();
if(fla)x=-x;
}
template <typename T> void printe(const T x)
{
if(x>=10)printe(x/10);
putchar(x%10+'0');
}
template <typename T> inline void print(const T x)
{
if(x<0)putchar('-'),printe(-x);
else printe(x);
}
const int maxn=10001,maxm=20001;
int n,m;
int head[maxn],tow[maxm],vau[maxm],nxt[maxm],tmp;
int Hash[maxn],son[maxn],size,dis[maxn];
int ans[10000001];
inline void addb(const int u,const int v,const int w)
{
tmp++;
nxt[tmp]=head[u];
head[u]=tmp;
tow[tmp]=v;
vau[tmp]=w;
}
int minn,root;
void getroot(const int u,const int fa)
{
son[u]=1;
int maxx=0;
for(register int i=head[u];~i;i=nxt[i])
{
const int v=tow[i];
if(v!=fa&&!Hash[v])
{
getroot(v,u);
son[u]+=son[v];
maxx=max(maxx,son[v]);
}
}
maxx=max(maxx,size-son[u]);
if(maxx<minn)minn=maxx,root=u;
}
int Q[maxn],tot;
void dfs(const int u,const int fa)
{
Q[++tot]=u;
for(register int i=head[u];~i;i=nxt[i])
{
const int v=tow[i];
if(v!=fa&&!Hash[v])
{
dis[v]=dis[u]+vau[i];
dfs(v,u);
}
}
}
void calc(const int u,const bool sign,const int more)
{
tot=0;
dis[u]=more;
dfs(u,u);
for(register int i=1;i<=tot;i++)
for(register int j=i+1;j<=tot;j++)
if(dis[Q[i]]+dis[Q[j]]<=10000000)
{
if(sign)ans[dis[Q[i]]+dis[Q[j]]]++;
else ans[dis[Q[i]]+dis[Q[j]]]--;
}
}
void solve(const int u,const int SIZE,const int SON)
{
calc(u,1,0);
Hash[u]=1;
for(register int i=head[u];~i;i=nxt[i])
{
const int v=tow[i];
if(!Hash[v])
{
calc(v,0,vau[i]);
minn=0x7fffffff,size=son[v];
if(size>SON)size=SIZE-SON;
getroot(v,u);
solve(root,size,son[root]);
}
}
}
int main()
{
memset(head,-1,sizeof(head));
read(n),read(m);
for(register int i=1;i<n;i++)
{
int u,v,w;read(u),read(v),read(w);
addb(u,v,w),addb(v,u,w);
}
minn=0x7fffffff;
size=n;
getroot(1,1);
solve(root,size,son[root]);
for(register int i=1;i<=m;i++)
{
LL k;
read(k);
if(ans[k])puts("AYE");
else puts("NAY");
}
return flush(),0;
}
看懂代码了吗?那个函数的作用是获取一个子树当中的一个点使得当前这个子树以那个点为根时的最大子树最小,这样就能保证树的深度每增加一层,当前子树的大小就会减少一半,所以是O(logn)层而不会退化到O(n)层
这样一来呢,复杂度差不多是O(n^2)但由于常数优化和算法上的1/2常数,自然轻松跑过啦
补充2
update by 2019.1.9
补充1的例题复杂度是
Θ
(
n
2
)
\Theta(n^2)
Θ(n2)的,可能会给初学者带去误解
所以特意来补一题,这一题的复杂度是
Θ
(
n
l
o
g
2
n
)
\Theta(nlog^2n)
Θ(nlog2n)的
点分治是
Θ
(
n
l
o
g
n
)
\Theta(nlogn)
Θ(nlogn),套上的树状数组复杂度
Θ
(
l
o
g
n
)
\Theta(logn)
Θ(logn)
[USACO18JAN][luoguP4183 ]Cow at Large P
总结
对于点分治,我就是那么学下来的吧,我觉得它的精髓在于两句话
1.对于端点是自己子树上的任意一条路径,它要么经过自己,要么在自己的某个子树中
2.获取树的重心来把树的深度从O(n)变到O(logn)
总的来说,感觉点分治还是挺有用的,学一下说不定之后会用到