【JZOJ5678】【GDOI2018Day2模拟4.21】果树(矩阵覆盖问题)

Problem

  NiroBC 姐姐是个活泼的少女,她十分喜欢爬树,而她家门口正好有一棵果树,正好满足了她爬树的需求。
  这颗果树有N个节点,节点标号 1…N。每个节点长着一个果子,第i个节点上的果子颜色为 Ci 。
  NiroBC姐姐每天都要爬树,每天都要选择一条有趣的路径 (u,v) 来爬。
  一条路径被称作有趣的,当且仅当这条路径上的果子的颜色互不相同。
  (u,v) 和 (v,u) 被视作同一条路径。特殊地,(i,i) 也被视作一条路径,这条路径只含 i 一个果子,显然是有趣的。
  NiroBC姐姐想知道这颗树上有多少条有趣的路径。

Hint

这里写图片描述

Solution

  这题我刚看以为是启发式合并,再想想又感觉可能可以点分治,不过想了个把小时都没想到,最终只得无奈地打了67points的做法。

12points:暴力

  两重循环,枚举一条路径的两个端点,然后再用普通lca暴力判断路径中的颜色是否互不相同。
  时间复杂度: O(N3) O ( N 3 )

Code

  实在太水了,故没打。

37points:dfs

  考虑枚举一条路径的一个端点x。
  我们可以设x为root,然后从x开始dfs。
  显然,我们在dfs到x这个节点(不一定是root)的时候,可以维护从root到x中出现过哪些颜色,然后只走向那些没有出现过的颜色的子节点。
  时间复杂度: O(N2) O ( N 2 )

Code

  反正下面的67points做法也有,故不单独贴了。

67points:dfs+队列

  Subtask3的特殊限制是整棵树形成一条链。那么我们也可以把那棵树代换成一个序列,则原问题转化为:1.定义一个有趣的子串为序列中的一个连续的子串,且该子串中每个位置的颜色不重复;2.求序列中有趣的子串数。
  考虑枚举子串的右边界i。
  若设当右边界为i时,最大的有趣子串的左边界为j,则可以发现,j随着i的递增严格单调不减。于是我们就可以像一个队列一样,每次插入c[i]这个颜色,而如果原先的最大的有趣子串中已包含这个颜色,则让j++,直到不包含为止。
  结合前面37points的做法,可以拿到67points。
  时间复杂度: O(N2) O ( N 2 ) or O(N) O ( N )

Code
#include <cstdio>
using namespace std;
#define ll long long
#define fo(i,a,b) for(i=a;i<=b;i++)
const int N=1e5+1,M=N<<1;
int i,j,n,c[N],u,v,tot,tov[M],next[M],last[N];
ll ans;
bool color[N];

inline void insert(int x,int y)
{
    tov[++tot]=y;
    next[tot]=last[x];
    last[x]=tot;
}
void scan()
{
    scanf("%d",&n);
    fo(i,1,n)scanf("%d",&c[i]);
    fo(i,1,n-1)
    {
        scanf("%d%d",&u,&v);
        insert(u,v);
        insert(v,u);
    }
}

void dfs(int x,int fat)
{
    color[c[x]]=1;
    int i,y;
    for(i=last[x];i;i=next[i])
        if((y=tov[i])!=fat&&!color[c[y]])
            ans++,dfs(y,x);
    color[c[x]]=0;
}
void brute()
{
    fo(i,1,n)dfs(i,0);
    ans=ans/2+(ll)n;
}

void limp()
{
    j=1;
    fo(i,1,n)
    {
        while(j<i&&color[c[i]])color[c[j++]]=0;
        color[c[i]]=1;
        ans+=(ll)(i-j+1);
    }
}

int main()
{
    freopen("tree.in","r",stdin);
    freopen("tree.out","w",stdout);
    scan();
    if(n<=3e3)
            brute();
    else    limp();
    printf("%lld",ans);
}
100points:扫描线+线段树

  一开始我也觉得很扯:这道题怎么会和扫描线+线段树有关系?!
  但是其实这就是正解。
  这道题和子树看上去有点关系,考虑先将整棵树dfs一遍,求出每个点的dfs序(记为dfn,先序遍历),以及以每个节点为根的子树的编号区间。设p[x]为点x的子树的左编号(显然就等于dfn[x]),q[x]为右编号。
  然而这并没有什么卵用。
  
  我们可以考虑一个问(when)题:
  给定一棵N个点的树,给出M组形如“x与y不可出现在同一路径上”的限制,问有多少条合法路径。
  如果一条路径(u,v)不合法,就说明(u,v)上同时出现了某组x,y。
  考虑把路径(u,v)映射成二维直角坐标系上的一个点,横坐标是dfn[u],纵坐标是dfn[v]。设dfn[u]≥dfn[v]。
  我们可以分类讨论一下:

  1. 当x和y有祖孙关系时。设x是y的祖先,z为x到y的路径上与x距离为1的点,则u和v两个点,一个 不在 z的子树内,另一个 y的子树内。也就是说,x和y这组限制使得坐标系中[p[y]..q[y],1..p[z]-1]这个矩阵(显然z亦为y的祖先,所以dfn[z]<dfn[y],于是q[y]≥p[y]>p[z]-1)不合法,并使[q[z]+1..n,p[y]..q[y]](显然y在z的子树中,所以q[z]≥q[y],于是q[z]+1>q[y]≥p[y])这个矩阵也不合法。

  2. 当x和y无祖孙关系时。u和v两个点,一个 x的子树内,另一个 y的子树内。设dfn[x]≥dfn[y],则x和y这组限制使得[p[x]..q[x],p[y]..q[y]]这个矩阵不合法。

      这时,问题就转化成了一个N*N的二维平面,上面有至多2M个矩形,求有多少个格子不被任何一个矩形覆盖
      这种题可以用二维线段树/树套树扫描线+线段树轻松解决。
      不过,要注意一点,此问题求的并不是往常的“所有点中被覆盖的最多次数”之类的,而是没被覆盖的点数,所以我们在实现线段树时不能傻傻地直接区间修改,而是要修改它的lazy_tag,且这个lazy_tag反常地不能下传,然后以lazy_tag来维护线段树t。
      
      回到这道题目,要求路径上的点的颜色互不相同,所以同色的点不能出现在同一路径上。对于每一个点,最多只有19个点与其颜色相同,所以M至多=19N。
      时间复杂度: O(19Nlog2N) O ( 19 N l o g 2 N )

Code
#include <cstdio>
#include <algorithm>
using namespace std;
#define ll long long
#define fo(i,a,b) for(i=a;i<=b;i++)
#define fd(i,a,b) for(i=a;i>=b;i--)
const int N=1e5+9,M=N*20;
int i,j,c[N],u,v,tot,tov[N<<1],next[N<<1],last[N],dfn[N],p[N],q[N],time,deep[N],anc[N][17],b[N],x,y;
ll n,ans;

inline void link(int x,int y)
{
    tov[++tot]=y;
    next[tot]=last[x];
    last[x]=tot;
}
void scan()
{
    scanf("%d",&n);
    fo(i,1,n)scanf("%d",&c[i]);
    fo(i,1,n-1)
    {
        scanf("%d%d",&u,&v);
        link(u,v);
        link(v,u);
    }
}

void renum(int x)
{
    dfn[x]=p[x]=++time;
    int i,y=anc[x][0];
    fo(i,1,16)
    {
        if(!y)break;
        y=anc[x][i]=anc[y][i-1];
    }

    for(i=last[x];i;i=next[i])
        if(!dfn[y=tov[i]])
        {
            deep[y]=deep[x]+1;
            anc[y][0]=x;
            renum(y);
        }
    q[x]=time;
}

inline bool cmp(int x,int y){return c[x]<c[y];}
void init()
{
    renum(1);
    fo(i,1,n)b[i]=i;
    sort(b+1,b+n+1,cmp);
}

int getz(int x,int y)
{
    int i,f;
    fd(i,16,0)if(deep[f=anc[y][i]]>deep[x])y=f;
    return y;
}
bool pdf(int x,int y)
{
    return anc[getz(x,y)][0]==x;
}
struct Limit
{
    int P[M<<1],Q[M<<1],W[M<<1],next[M<<1],last[N],tot;
    inline void link(int x,int p,int q,int w)
    {
        P[++tot]=p;Q[tot]=q;W[tot]=w;
        next[tot]=last[x];
        last[x]=tot;
    }
    inline void limit(int x,int y)
    {
        if(pdf(x,y))
        {
            int z=getz(x,y);
            link(p[y]  ,1   ,p[z]-1,1 );
            link(q[y]+1,1   ,p[z]-1,-1);
            link(q[z]+1,p[y],q[y]  ,1 );
        }
        else
        {
            if(dfn[x]<dfn[y])swap(x,y);
            link(p[x]  ,p[y],q[y],1 );
            link(q[x]+1,p[y],q[y],-1);
        }
    }
}L;
void cover()
{
    fo(i,1,n)
        fd(j,i-1,1)
        {
            if(c[x=b[i]]>c[y=b[j]])break;
            if(deep[x]>deep[y])swap(x,y);
            L.limit(x,y);
        }
}

#define A v<<1
#define B A|1
int val,t[N<<2],lz[N<<2];
void add(int v,int l,int r)
{
    if(y<l||x>r)return;
    if(x<=l&&r<=y){lz[v]+=val;return;}
    int mid=l+r>>1;
    add(A,l,mid);add(B,mid+1,r);
    t[v]=(lz[A]?mid-l+1:t[A])+(lz[B]?r-mid:t[B]);
}
void work()
{
    fo(i,1,n)
    {
        for(j=L.last[i];j;j=L.next[j])
        {
            x=L.P[j];y=L.Q[j];val=L.W[j];
            add(1,1,n);
        }
        ans+=lz[1]?i:t[1];
    }   
}

inline void print()
{
    ans=n*n-2*ans;
    printf("%lld",(ans-n)/2+n); 
}

int main()
{
    freopen("tree.in","r",stdin);
    freopen("tree.out","w",stdout);
    scan();
    init();
    cover();
    work();
    print();
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值