青云的机房组网方案(简单+普通+困难)(虚树+树形DP+容斥)

Posted chenhuan001

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了青云的机房组网方案(简单+普通+困难)(虚树+树形DP+容斥)相关的知识,希望对你有一定的参考价值。

题目链接

1.对于简单的版本n<=500, ai<=50

直接暴力枚举两个点x,y,dfs求x与y的距离。

 

2.对于普通难度n<=10000,ai<=500

普通难度解法挺多

第一种,树形dp+LCA

比赛的时候,我猜测对于不为1的n个数,其中两两互质的对数不会很多,肯定达不到n^2

然后找出所有互质的对数,然后对为1的数进行特殊处理。(初略的估计了下,小于500的大概有50个质数,将n个数平均分到这些数中,最后大概有10000*50*200=10^7)

对所有的非1质数对,采用离线LCA可以搞定。

对于1的特殊情况,只需要用两次dfs,就可以找出所有1到其它点的距离和与1之间的距离和。

 

第二种,树形dp+容斥

这种方法从边的角度,考虑每一条边会被计算多少次,这也是树上求距离的常用方法。

由于树边都是桥边,所有只要求出边两边联通块之间互质对数。最简单的想法即是枚举每一条边,然后再分别枚举两边区域,这样的复杂度是500*500*10000 很遗憾并没有这么简单。于是用容斥原理进行优化。在枚举某条边的一边的x(1<=x<=500)的时候,考虑右边为x质因子倍数的情况,也就是容斥了。 这样可以将复杂度变为10000*500*k*2^k( k<=4)

官方题解:

 

 附上代码:

//
//  main.cpp
//  160701
//
//  Created by New_Life on 16/7/1.
//  Copyright © 2016年 chenhuan001. All rights reserved.
//

#include <iostream>
#include <stdio.h>
#include <string.h>
#include <vector>
#include <algorithm>
using namespace std;
#define N 10100

vector<int> save[505];
int g[N];
struct node
{
    int to,next;
}edge[2*N];

int cnt,pre[N];
int dp[N][505];
int savecnt[N][505];
int cntall[505];
long long ans;

void add_edge(int u,int v)
{
    edge[cnt].to = v;
    edge[cnt].next = pre[u];
    pre[u] = cnt++;
}

void dfs(int s,int fa)
{
    for(int p=pre[s];p!=-1;p=edge[p].next)
    {
        int v = edge[p].to;
        if(v == fa) continue;
        dfs(v,s);
        for(int i=1;i<=500;i++)
        {
            dp[s][i] += dp[v][i];
            savecnt[s][i] += savecnt[v][i];
        }
    }
    
    savecnt[s][ g[s] ]++;

    for(int i=0;i<(1<<save[g[s]].size());i++)
    {
        int tmp = 1;
        for(int j=0;j<save[g[s]].size();j++)
        {
            if(((1<<j)&i) != 0)
            {
                tmp *= save[g[s]][j];
            }
        }
        dp[s][tmp]++;
    }
    
    //int last[505];
    int lastcnt[505];
    for(int p=pre[s];p!=-1;p=edge[p].next)
    {
        int v = edge[p].to;
        if(v == fa) continue;
        for(int i=1;i<=500;i++)
        {
            //last[i] = all[i]-dp[v][i];
            lastcnt[i] = cntall[i]-savecnt[v][i];
        }
        //对这条边进行处理
        for(int i=1;i<=500;i++)
        {
            if(lastcnt[i] == 0) continue;
            for(int j=0;j<(1<<save[i].size());j++)
            {
                int tcnt=0;
                int tnum = 1;
                for(int k=0;k<save[i].size();k++)
                {
                    if( ((1<<k)&j)!=0 )
                    {
                        tcnt++;
                        tnum *= save[i][k];
                    }
                }
                if(tcnt%2 == 0) ans += lastcnt[i]*dp[v][tnum];
                else ans -= lastcnt[i]*dp[v][tnum];
            }
        }
    }
    
}

int main(int argc, const char * argv[]) {
    for(int i=1;i<=500;i++)
    {
        int ti = i;
        for(int j=2;j*j<=ti;j++)
        {
            if(ti%j == 0)
            {
                save[i].push_back(j);
                while(ti%j==0) ti/=j;
            }
        }
        if(ti != 1) save[i].push_back(ti);
    }
    //for(int i=1;i<=500;i++) printf("%d\\n",save[i].size());
    cnt = 0;
    memset(pre,-1,sizeof(pre));
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",g+i);//得把每一项都变成最简单
        int tg =1;
        for(int j=0;j<save[ g[i] ].size();j++)
        {
            tg *= save[ g[i] ][j];
        }
        g[i] = tg;
    }
    for(int i=1;i<n;i++)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        add_edge(a,b);
        add_edge(b,a);
    }
    ans = 0;
    for(int ii=1;ii<=n;ii++)
    {
        cntall[ g[ii] ]++;
    }
    
    dfs(1,-1);
    cout<<ans<<endl;
    return 0;
}

 

3. 对于困难难度

这就需要用到虚树这种没听过的东西了,百度学习下,然后发现原理还是很简单的。应用情景,对于一颗树,挺大,但是需要操作的结点不多,这时候把需要操作的结点重新建一颗小的树(需要用的信息不能少)。 

思路概述:(抄了下)

  1. 枚举 因数x,x是每种质因子至多有一个的数,记录一下x有几种质因子,方便之后容斥。
  2. 把所有x的倍数的权值的点找出来,预处理下可以做到找出来的点的dfs序是从小到大的,预处理也可以使得每次找x的倍数的权值的点不必线性扫一遍。
  3. 然后对这些点 O(n) 建虚树,具体操作是相邻两个点加进去 lca,用一个栈维护下父亲链即可。[bzoj3572]是一道典型的虚树的题目。
  4. 构建好树后在树上 dfs 两次可以求出所有x的倍数的权值的点对之间的距离和,就是第一遍dfs记录以节点u为根的子树中,有多少个x倍数的点(可能有一些是虚树添加进来的lca点),第二遍dfs其实是枚举每条边,计算(u,v)这条边的总价值,就是它出现的次数乘以它的权值;它出现的次数就是它子树中x倍数的点的个数,乘以不在它子树中x倍数的点的个数。
  5. 最后容斥下就可以求出答案。

由于所有步骤均是线性的,而所有虚树加起来的总点数也是线性乘上一个常数的,所以复杂度为 O(nK),K<=128。

对于复杂度分析,我抱有不同的看法,上述过程中建虚树是O(nlog(n))的,100000以内不重复质数最多是6个,所以最大复杂度为O(64*n*log(n))

//
//  main.cpp
//  Xushu
//
//  Created by New_Life on 16/7/1.
//  Copyright © 2016年 chenhuan001. All rights reserved.
//

#include <iostream>
#include <stdio.h>
#include <string.h>
#include <vector>
#include <algorithm>
using namespace std;

#define N 100100
#define LN 20

struct node
{
    int to,next;
}edge[2*N];

int cnt,pre[N];

void add_edge(int u,int v)
{
    edge[cnt].to = v;
    edge[cnt].next = pre[u];
    pre[u] = cnt++;
}

int deep[N];
int g[N];//记录每个点的权值
vector<int>saveall[N];//记录i所有的倍数
int sign[N];
int len[N];//每个点到根的距离
int mark[N];//标示虚树上的点是否是无用点

struct Lca_Online
{
    int _n;
    
    int dp[N][LN];
    
    void _dfs(int s,int fa,int dd)
    {
        int factor[30];
        int fcnt=0;
        int tmp = g[s];
        for(int i=2;i*i<=tmp;i++)
        {
            if(tmp%i == 0)
            {
                factor[ fcnt++ ] = i;
                while(tmp%i == 0) tmp/=i;
            }
        }
        if(tmp != 1) factor[ fcnt++ ] = tmp;
        
        for(int i=0;i<(1<<fcnt);i++)
        {
            tmp = 1;
            int tsign = 1;
            for(int j=0;j<fcnt;j++)
                if( ((1<<j)&i) != 0 )
                {
                    tmp *= factor[j];
                    tsign *= -1;
                }
            saveall[tmp].push_back(s);
            sign[tmp] = tsign;
            
        }
        
        deep[s] = dd;
        for(int p=pre[s];p!=-1;p=edge[p].next)
        {
            int v = edge[p].to;
            if(v == fa) continue;
            _dfs(v,s,dd+1);
            dp[v][0] = s;
        }
    }
    
    void _init()
    {
        for(int j=1;(1<<j)<=_n;j++)
        {
            for(int i=1;i<=_n;i++)
            {
                if(dp[i][j-1]!=-1) dp[i][j] = dp[ dp[i][j-1] ][j-1];
            }
        }
    }
    void lca_init(int n)
    {
        _n = n;
        memset(dp,-1,sizeof(dp));
        //_dfs(firstid,-1,0);
        _dfs(1,-1,0);
        _init();
    }
    
    int lca_query(int a,int b)
    {
        if(deep[a]>deep[b]) swap(a,b);
        //调整b到a的同一高度
        for(int i=LN-1;deep[b]>deep[a];i--)
            if(deep[b]-(1<<i) >= deep[a]) b = dp[b][i];
        if(a == b) return a;
        for(int i=LN-1;i>=0;i--)
        {
            if(dp[a][i]!=dp[b][i]) a = dp[a][i],b = dp[b][i];
        }
        return dp[a][0];
    }
}lca;

int stk[N],top;
vector<int>tree[N];//存边
vector<int>treew[N];//存权

void tree_add(int u,int v,int w)
{
    tree[u].push_back(v);
    tree[v].push_back(u);
    treew[u].push_back(w);
    treew[v].push_back(w);
}

long long down[N];
long long ccnt[N];
long long sum[N];
int nn;

void dfs1(int s,int fa)
{
    down[s] = 0;
    ccnt[s] = 0;
    for(int i=0;i<tree[s].size();i++)
    {
        int to = tree[s][i];
        if(to == fa) continue;
        dfs1(to,s);
        down[s] += down[to] + ccnt[to]*treew[s][i];
        ccnt[s] += ccnt[to];
    }
    if(mark[s]==1)
        ccnt[s]++;
}

void dfs2(int s,int fa,long long num,long long tcnt)
{
    sum[s] = down[s]+num+tcnt;
    for(int i=0;i<tree[s].size();i++)
    {
        int to = tree[s][i];
        if(to == fa) continue;
        dfs2(to,s,sum[s]-down[to]-ccnt[to]*treew[s][i],(nn-ccnt[to])*treew[s][i]);
    }
}

int main(int argc, const char * argv[]) {
    cnt = 0;
    memset(pre,-1,sizeof(pre));
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",g+i);
    }
    for(int i=1;i<n;i++)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        add_edge(a, b);
        add_edge(b, a);
    }
    
    lca.lca_init(n);
    long long ans=0;
    
    for(int x=1;x<=100000;x++)
    {
        if(saveall[x].size() == 0) continue;
        //build virtual tree
        top = 0;
        
        stk[top++] = saveall[x][0];
        tree[ saveall[x][0] ].clear();
        treew[ saveall[x][0] ].clear();
        mark[saveall[x][0]]=1;
        for(int i=1;i<saveall[x].size();i++)
        {
            int v = saveall[x][i];
            
            int plca = lca.lca_query(stk[top-1], v);//最近公共祖先
            if(plca == stk[top-1]) ;//不处理
            else
            {
                
                int pos=top-1;
                while(pos>=0 && deep[ stk[pos] ]>deep[plca])
                    pos--;
                pos++;
                for(int j=pos;j<top-1;j++)
                {
                    tree_add(stk[j],stk[j+1],deep[stk[j+1]]-deep[stk[j]]);
                }
                int prepos = stk[pos];
                if(pos == 0)
                {
                    tree[plca].clear(),treew[plca].clear(),stk[0]=plca,top=1;
                    mark[plca] = 0;
                }
                else if(stk[pos-1] != plca)
                {
                    tree[plca].clear(),treew[plca].clear(),stk[pos]=plca,top=pos+1;
                    mark[plca] = 0;
                }
                else top = pos;
                tree_add(prepos,plca,deep[prepos]-deep[plca]);
                
            }
            tree[v].clear();
            treew[v].clear();
            stk[top++] = v;
            mark[v] = 1;
        }
        for(int i=0;i<top-1;i++)
        {
            tree_add(stk[i], stk[i+1], deep[stk[i+1]]-deep[stk[i]]);
        }
        //构建好了虚树,然后就是两次dfs
        nn = (int)saveall[x].size();
        dfs1(saveall[x][0],-1);
        dfs2(saveall[x][0],-1,0,0);
        long long tans=0;
        for(int i=0;i<saveall[x].size();i++)
            tans += sum[ saveall[x][i] ];
        tans /= 2;
        
        ans += sign[x]*tans;
    }
     
    cout<<ans<<endl;//时间,内存。
    
    return 0;
}

 

以上是关于青云的机房组网方案(简单+普通+困难)(虚树+树形DP+容斥)的主要内容,如果未能解决你的问题,请参考以下文章

青云的机房组网方案(中等) 计蒜客

BZOJ 2286 消耗战 (虚树+树形DP)

bzoj3611

BZOJ 2286 树链剖分+DFS序+虚树+树形DP

BZOJ-3572世界树 虚树 + 树形DP

CF613DKingdom and its Cities 虚树+树形DP