同步与 Luogu blog
Link
https://www.luogu.com.cn/problem/P3769
Description
给定 \(n\) 个四维点,第 \(i\) 个点为 \((x_i,y_i,z_i,t_i)\) 。
对于一个合法的路径,满足经过的点的四个坐标全部单调不降。
路径的长度定义为该路径所经过点的个数。
求合法路径长度的最大值。
Hint
\(1\le n\le 5\times 10^4, x_i,y_i,z_i,t_i\in[0,10^9)\)
Soluiton
最简单的,可以动规。设 \(f_i\) 为点 \(1\cdots i\) 中的答案。那么有:
\[f_i = \max\{f_j|x_i\ge x_j,y_i\ge y_j,z_i\ge z_j,t_i\ge t_j\} +1\]
然而这样一看就超时,一是因为这是平方级别的,二是不好优化处理。
很容易想到先按 \(x\) 升序排序(直接白给一维),那么方程变成了这样:
\[f_i = \max\{f_j|i>j,y_i\ge y_j,z_i\ge z_j,t_i\ge t_j\} +1\]
但这样还是需要平方级别的时间。
我们发现现在这个其实就是一个经典的 三维偏序 (有一位排序优化掉了)问题,而且必须在线处理。
显然,K-D Tree
可以用来优化。
我们在 KDT 的结点中存以下信息:
子树矩形边界,子树 \(\operatorname{size}\)。
结点对应点的信息,包含后三个维度的信息,以及该点的点权。这个点权又是怎么回事呢?详见下面。
子树点权和。
其实,我们只要把上面的那个 \(f\) 作为点的点权插入到 KDT 中就行了。具体地:
初始树为空。对于一个将要查询的点 \((a_i,b_i,c_i)\),我们先找出所有满足点的三个坐标分别 \(\le\) 该点的三个坐标的点,然后算出这些点的点权的最大值\(+1\),记作 \(\operatorname{tmp}_i\) 。然后把点 \((a_i,b_i,c_i)\) 以 \(\operatorname{tmp}_i\) 为点权插入到 KDT 中。
如此做 \(n\) 次,最终答案即为 \(\max\limits_{i=1}^{n} \operatorname{tmp}_i\) 的值。
Code Ver.1
#include<cstdio>
#include<algorithm>
#include<stack>
using namespace std;
const int N=5e4+5;
struct Point{
int dat[3],val;
Point(){}
Point(int x,int y,int z,int v=0){dat[0]=x,dat[1]=y,dat[2]=z,val=v;}
}pnt[N];
namespace KDT
{
const int alpha=0.75;
#define lc(x) (t[x].ch[0])
#define rc(x) (t[x].ch[1])
struct Node{
int ch[2];
int Max[3],Min[3];
int maxv;
int size;
Point p;
}t[N];
int total=0,root=0;
inline void push_up(const int &u)
{
for(register int i=0;i<3;i++)
{
t[u].Max[i]=t[u].Min[i]=t[u].p.dat[i];
if(lc(u))
{
t[u].Max[i]=max(t[u].Max[i],t[lc(u)].Max[i]);
t[u].Min[i]=min(t[u].Min[i],t[lc(u)].Min[i]);
}
if(rc(u))
{
t[u].Max[i]=max(t[u].Max[i],t[rc(u)].Max[i]);
t[u].Min[i]=min(t[u].Min[i],t[rc(u)].Min[i]);
}
}
t[u].size=t[lc(u)].size+t[rc(u)].size+1;
t[u].maxv=max(max(t[lc(u)].maxv,t[rc(u)].maxv),t[u].p.val);
}
namespace Rebuild
{
Point buf[N];
stack<int> pool;
int len=0,dim;
inline bool imbalanced(const int &u)
{
if((double)t[u].size*alpha>(double)t[lc(u)].size) return true;
if((double)t[u].size*alpha>(double)t[rc(u)].size) return true;
return false;
}
inline bool cmp(const Point &a,const Point &b)
{
return a.dat[dim]<b.dat[dim];
}
void travel(const int &u)
{
if(!u) return;
pool.push(u);
buf[++len]=t[u].p;
travel(lc(u));
travel(rc(u));
}
int build(int l,int r,int d)
{
if(l>r) return 0;
int mid=(l+r)>>1,u=pool.top();
pool.pop(),dim=d;
nth_element(buf+l,buf+mid,buf+r+1,cmp);
t[u].p=buf[mid];
lc(u)=build(l,mid-1,(d+1)%3);
rc(u)=build(mid+1,r,(d+1)%3);
return push_up(u),u;
}
void work(int &u,const int &d)
{
if(!imbalanced(u)) return;
len=0,travel(u);
u=build(1,len,d);
}
}
void insert(const Point &x,int &u=root,int d=0)
{
if(!u)
{
u=++total;
t[u].p=x;
lc(u)=rc(u)=0;
push_up(u);
return;
}
if(x.dat[d]<=t[u].p.dat[d])
insert(x,lc(u),(d+1)%3);
else
insert(x,rc(u),(d+1)%3);
push_up(u);
Rebuild::work(u,d);
}
int query(const int &x,const int &y,const int &z,const int &u=root)
{
if(!u) return 0;
if(x<t[u].Min[0]||y<t[u].Min[1]||z<t[u].Min[2])
return 0;
if(x>=t[u].Max[0]&&y>=t[u].Max[1]&&z>=t[u].Max[2])
return t[u].maxv;
int ret=0;
if(x>=t[u].p.dat[0]&&y>=t[u].p.dat[1]&&z>=t[u].p.dat[2])
ret=t[u].p.val;
ret=max(ret,query(x,y,z,lc(u)));
ret=max(ret,query(x,y,z,rc(u)));
return ret;
}
#undef lc
#undef rc
}
struct Item{
int i,j,k,l;
bool operator <(const Item &t) const{
return (i<t.i)||(i==t.i&&j<t.j)||(i==t.i&&j==t.j&&k<t.k)||(i==t.i&&j==t.j&&k==t.k&&l<t.l);
}
}rec[N];
int n;
signed main()
{
scanf("%d",&n);
for(register int i=1;i<=n;i++)
scanf("%d%d%d%d",&rec[i].i,&rec[i].j,&rec[i].k,&rec[i].l);
sort(rec+1,rec+1+n);
for(register int i=1;i<=n;i++)
pnt[i]=Point(rec[i].j,rec[i].k,rec[i].l);
int ans=0;
for(register int i=1;i<=n;i++)
{
int tmp=KDT::query(pnt[i].dat[0],pnt[i].dat[1],pnt[i].dat[2])+1;
ans=max(ans,tmp);
pnt[i].val=tmp;
KDT::insert(pnt[i]);
}
printf("%d\n",ans);
return 0;
}
然后:https://www.luogu.com.cn/record/31807909
为什么TLE??
是因为没有剪枝!
optimizing 1
加了这个优化跑的飞快!
如果对于一次查询,做了一半,得到的返回值的最大值 \(\operatorname{cur}\) (下面代码称 ret
) 。
若当前子树的点权最大值 \(\le \operatorname{cur}\) ,那么可以考虑停止往下计算。因为该子树的答案对结果无任何贡献,说白了就是算了白算。
Code Ver. 2
#include<cstdio>
#include<algorithm>
#include<stack>
using namespace std;
const int N=5e4+5;
struct Point{
int dat[3],val;
Point(){}
Point(int x,int y,int z,int v=0){dat[0]=x,dat[1]=y,dat[2]=z,val=v;}
}pnt[N];
namespace KDT
{
const int alpha=0.75;
#define lc(x) (t[x].ch[0])
#define rc(x) (t[x].ch[1])
struct Node{
int ch[2];
int Max[3],Min[3];
int maxv;
int size;
Point p;
}t[N];
int total=0,root=0;
inline void push_up(const int &u)
{
for(register int i=0;i<3;i++)
{
t[u].Max[i]=t[u].Min[i]=t[u].p.dat[i];
if(lc(u))
{
t[u].Max[i]=max(t[u].Max[i],t[lc(u)].Max[i]);
t[u].Min[i]=min(t[u].Min[i],t[lc(u)].Min[i]);
}
if(rc(u))
{
t[u].Max[i]=max(t[u].Max[i],t[rc(u)].Max[i]);
t[u].Min[i]=min(t[u].Min[i],t[rc(u)].Min[i]);
}
}
t[u].size=t[lc(u)].size+t[rc(u)].size+1;
t[u].maxv=max(max(t[lc(u)].maxv,t[rc(u)].maxv),t[u].p.val);
}
namespace Rebuild
{
Point buf[N];
stack<int> pool;
int len=0,dim;
inline bool imbalanced(const int &u)
{
if((double)t[u].size*alpha>(double)t[lc(u)].size) return true;
if((double)t[u].size*alpha>(double)t[rc(u)].size) return true;
return false;
}
inline bool cmp(const Point &a,const Point &b)
{
return a.dat[dim]<b.dat[dim];
}
void travel(const int &u)
{
if(!u) return;
pool.push(u);
buf[++len]=t[u].p;
travel(lc(u));
travel(rc(u));
}
int build(int l,int r,int d)
{
if(l>r) return 0;
int mid=(l+r)>>1,u=pool.top();
pool.pop(),dim=d;
nth_element(buf+l,buf+mid,buf+r+1,cmp);
t[u].p=buf[mid];
lc(u)=build(l,mid-1,(d+1)%3);
rc(u)=build(mid+1,r,(d+1)%3);
return push_up(u),u;
}
void work(int &u,const int &d)
{
if(!imbalanced(u)) return;
len=0,travel(u);
u=build(1,len,d);
}
}
void insert(const Point &x,int &u=root,int d=0)
{
if(!u)
{
u=++total;
t[u].p=x;
lc(u)=rc(u)=0;
push_up(u);
return;
}
if(x.dat[d]<=t[u].p.dat[d])
insert(x,lc(u),(d+1)%3);
else
insert(x,rc(u),(d+1)%3);
push_up(u);
Rebuild::work(u,d);
}
void query(const int &x,const int &y,const int &z,int &ret,const int &u=root)
{
if(!u) return;
if(ret>=t[u].maxv)//剪枝在这里!!!
return;
if(x<t[u].Min[0]||y<t[u].Min[1]||z<t[u].Min[2])
return;
if(x>=t[u].Max[0]&&y>=t[u].Max[1]&&z>=t[u].Max[2])
return void(ret=max(ret,t[u].maxv));
if(x>=t[u].p.dat[0]&&y>=t[u].p.dat[1]&&z>=t[u].p.dat[2])
ret=max(ret,t[u].p.val);
query(x,y,z,ret,lc(u)),query(x,y,z,ret,rc(u));
}
#undef lc
#undef rc
}
struct Item{
int i,j,k,l;
bool operator <(const Item &t) const{
return (i<t.i)||(i==t.i&&j<t.j)||(i==t.i&&j==t.j&&k<t.k)||(i==t.i&&j==t.j&&k==t.k&&l<t.l);
}
}rec[N];
int n;
signed main()
{
scanf("%d",&n);
for(register int i=1;i<=n;i++)
scanf("%d%d%d%d",&rec[i].i,&rec[i].j,&rec[i].k,&rec[i].l);
sort(rec+1,rec+1+n);
for(register int i=1;i<=n;i++)
pnt[i]=Point(rec[i].j,rec[i].k,rec[i].l);
int ans=0;
for(register int i=1;i<=n;i++)
{
int tmp=0;
KDT::query(pnt[i].dat[0],pnt[i].dat[1],pnt[i].dat[2],tmp);
ans=max(ans,++tmp);
pnt[i].val=tmp;
KDT::insert(pnt[i]);
}
printf("%d\n",ans);
return 0;
}
这下就可以愉快的AC了,时间上毫无压力:https://www.luogu.com.cn/record/31808114
然而待我翻了翻 Luogu 的题解区之后,才发现还能优化,然而我没去写,先把思路讲一下。
optimizing 2
我们发现所有可能的点就这 \(n\) 个,没跑。
所以可以先把树建好,点权均为 0,然后用修改值的操作替换插入。
这样似乎也可以快一些。详见:https://www.luogu.com.cn/blog/zltttt/solution-p3769