BZOJ1095 [ZJOI2007]Hide 捉迷藏

Description

  Jiajia和Wind是一对恩爱的夫妻,并且他们有很多孩子。某天,Jiajia、Wind和孩子们决定在家里玩捉迷藏游戏。他们的家很大且构造很奇特,由N个屋子和N-1条双向走廊组成,这N-1条走廊的分布使得任意两个屋子都互相可达。游戏是这样进行的,孩子们负责躲藏,Jiajia负责找,而Wind负责操纵这N个屋子的灯。在起初的时候,所有的灯都没有被打开。每一次,孩子们只会躲藏在没有开灯的房间中,但是为了增加刺激性,孩子们会要求打开某个房间的电灯或者关闭某个房间的电灯。为了评估某一次游戏的复杂性,Jiajia希望知道可能的最远的两个孩子的距离(即最远的两个关灯房间的距离)。 我们将以如下形式定义每一种操作: C(hange) i 改变第i个房间的照明状态,若原来打开,则关闭;若原来关闭,则打开。 G(ame) 开始一次游戏,查询最远的两个关灯房间的距离。
  
Input
  第一行包含一个整数N,表示房间的个数,房间将被编号为1,2,3…N的整数。接下来N-1行每行两个整数a, b,表示房间a与房间b之间有一条走廊相连。接下来一行包含一个整数Q,表示操作次数。接着Q行,每行一个操作,如上文所示。

Output
  对于每一个操作Game,输出一个非负整数到hide.out,表示最远的两个关灯房间的距离。若只有一个房间是关着灯的,输出0;若所有房间的灯都开着,输出-1。

Sample Input
8

1 2

2 3

3 4

3 5

3 6

6 7

6 8

7

G

C 1

G

C 2

G

C 1

G

Sample Output
4

3

3

4

HINT
对于100%的数据, N ≤100000, M ≤500000。

第一次写动态点分治OwO 其实就是把每次找到的重心连成一棵树,这样每改变一个点的状态只要更新它在新树中的位置到根的一条链就行啦。
维护3种堆,C(n个)堆里放每个点的子树中所有点到它的父重心(即在新树中的父亲)的距离。B(n个)堆里放每个点与它的每个子树中的最大距离,即在它的子重心(即在新树中的儿子)的C堆中取MAX。A(1个)堆放每个点的答案,即所有B堆的最大值和次大值之和。
这样就好啦。啊还有要写可删堆,就是用2个堆,另一个存已经删了的元素。
不过我的程序好像比别人要慢很多,要8s+QwQ
这道题还有好多别的做法,括号序列+线段树(并不会),还有漆子超的论文《分治算法在树的路径问题中的应用》里写的边分治和树链剖分(把树链剖分理解为基于链的分治)+线段树,都好厉害啊OwO
#include <bits/stdc++.h>
#define N 100010
#define K 20
using namespace std;
int m,n,fir[N],to[N<<1],nxt[N<<1],tot(1),used[N<<1],size[N],ms[N];
int root,sum,col[N],h[N],g[N<<1][K],times(0),seq[N<<1],pos[N],lg[N<<1],rt,f[N];
char z[K];

struct heap{
    priority_queue<int> a,b;
    void push(int x){
        a.push(x);
    }
    void erase(int x){
        b.push(x);
    }
    int top(){
        while (!a.empty() && !b.empty() && a.top()==b.top())
            a.pop(),b.pop();
        return (a.empty()?-1:a.top());
    }
    int size(){
        return a.size()-b.size();
    }
    int sum(){
        if (size()<2)
            return -1;
        int s=0,t;
        t=s=top(); a.pop();
        s+=top(); a.push(t);
        return s;
    }
}A,B[N],C[N];

template <class Aqua>
inline void read(Aqua &s){
    s=0; char c=getchar();
    while (!isdigit(c)) c=getchar();
    while (isdigit(c)) s=s*10+c-'0',c=getchar();
}

inline void add(int u,int v){
    to[++tot]=v; nxt[tot]=fir[u]; fir[u]=tot;
}

void getrt(int x,int fa){
    size[x]=1; ms[x]=0;
    for (int i=fir[x];i;i=nxt[i])
        if (!used[i] && to[i]!=fa){
            getrt(to[i],x);
            ms[x]=max(ms[x],size[to[i]]);
            size[x]+=size[to[i]];
        }
    if (ms[x]*2<=sum && size[x]*2>=sum)
        root=x;
}

void dfs(int x,int fa){
    size[x]=1;
    for (int i=fir[x];i;i=nxt[i])
        if (!used[i] && to[i]!=fa){
            dfs(to[i],x);
            size[x]+=size[to[i]];
        }
}

void solve(int x){
    dfs(x,0);
    for (int i=fir[x];i;i=nxt[i])
        if (!used[i]){
            used[i]=used[i^1]=1;
            sum=size[to[i]]; getrt(to[i],0);
            f[root]=x; solve(root);
            used[i]=used[i^1]=0;
        }

}

void dfs_(int x,int fa){
    h[x]=h[fa]+1;
    seq[++times]=x; pos[x]=times;
    for (int i=fir[x];i;i=nxt[i])
        if (to[i]!=fa){
            dfs_(to[i],x);
            seq[++times]=x;
        }
}

inline int Min(int a,int b){
    return (h[a]<h[b]?a:b);
}

void pre(){
    sum=n; getrt(1,0); rt=root;
    solve(root);
    dfs_(1,0);
    for (int i=1;i<=times;i++)
        g[i][0]=seq[i];
    for (int i=1;i<K;i++)
        for (int j=1;j+(1<<i)-1<=times;j++)
            g[j][i]=Min(g[j][i-1],g[j+(1<<i-1)][i-1]);
    lg[1]=0;
    for (int i=2;i<=times;i++)
        lg[i]=lg[i>>1]+1;
}

inline int dis(int a,int b){
    if (pos[a]>pos[b]) swap(a,b);
    int k=lg[pos[b]-pos[a]+1];
    return h[a]+h[b]-2*h[Min(g[pos[a]][k],g[pos[b]-(1<<k)+1][k])];
}

void on(int x){
    int fa,tmp,d,pas,now;
    B[x].erase(0);
    if (B[x].size()==1)
        A.erase(B[x].top());
    for (int cur=x;(fa=f[cur]);cur=fa){
        tmp=C[cur].top(); d=dis(fa,x);
        C[cur].erase(d); d=C[cur].top();
        if (d<tmp){
            pas=B[fa].sum();
            if (~tmp) B[fa].erase(tmp);
            if (~d) B[fa].push(d);
            now=B[fa].sum();
            if (pas!=now){
                if (~pas) A.erase(pas);
                if (~now) A.push(now);
            }
        }
    }
}


void off(int x){
    int fa,tmp,d,pas,now;
    B[x].push(0);
    if (B[x].size()==2)
        A.push(B[x].top());
    for (int cur=x;(fa=f[cur]);cur=fa){
        tmp=C[cur].top(); d=dis(fa,x);
        C[cur].push(d);
        if (d>tmp){
            pas=B[fa].sum();
            if (~tmp) B[fa].erase(tmp);
            B[fa].push(d);
            now=B[fa].sum();
            if (now>pas){
                if (~pas) A.erase(pas);
                A.push(now);
            }
        }
    }
}

int main(){
    read(n);
    int u,v;
    for (int i=1;i<n;i++){
        read(u),read(v);
        add(u,v),add(v,u);
    }
    pre();
    int cnt=n;
    for (int i=1;i<=n;i++)
        off(i);
    read(m);
    for (int i=1;i<=m;i++){
        scanf("%s",z);
        if (z[0]=='G'){
            if (cnt>1)
                printf("%d\n",A.top());
            else
                printf("%d\n",cnt-1);
        }
        else{
            read(u); col[u]^=1;
            if (col[u])
                on(u),cnt--;
            else
                off(u),cnt++;
        }
    }
    return 0;
}
题目描述 有一个 $n$ 个的棋盘,每个上有一个数字 $a_i$,你需要从 $(1,1)$ 走到 $(n,n)$,每次只能往右或往下走,每个格子只能经过一次,路径上的数字和为 $S$。定义一个 $(x,y)$ 的权值为 $a_x+a_y$,求所有满足条件的路径中,所有的权值和的最小值。 输入格式 第一行一个整数 $n$。 接下来 $n$ 行,每行 $n$ 个整数,表示棋盘上每个的数字。 输出格式 输出一个整数,表示所有满足条件的路径中,所有的权值和的最小值。 数据范围 $1\leq n\leq 300$ 输入样例 3 1 2 3 4 5 6 7 8 9 输出样例 25 算法1 (树形dp) $O(n^3)$ 我们可以先将所有的权值求出来,然后将其看作是一个有权值的图,问题就转化为了在这个图中求从 $(1,1)$ 到 $(n,n)$ 的所有路径中,所有的权值和的最小值。 我们可以使用树形dp来解决这个问题,具体来说,我们可以将这个图看作是一棵树,每个的父节是它的前驱或者后继,然后我们从根节开始,依次向下遍历,对于每个节,我们可以考虑它的两个儿子,如果它的两个儿子都被遍历过了,那么我们就可以计算出从它的左儿子到它的右儿子的路径中,所有的权值和的最小值,然后再将这个值加上当前节的权值,就可以得到从根节到当前节的路径中,所有的权值和的最小值。 时间复杂度 树形dp的时间复杂度是 $O(n^3)$。 C++ 代码 算法2 (动态规划) $O(n^3)$ 我们可以使用动态规划来解决这个问题,具体来说,我们可以定义 $f(i,j,s)$ 表示从 $(1,1)$ 到 $(i,j)$ 的所有路径中,所有的权值和为 $s$ 的最小值,那么我们就可以得到如下的状态转移方程: $$ f(i,j,s)=\min\{f(i-1,j,s-a_{i,j}),f(i,j-1,s-a_{i,j})\} $$ 其中 $a_{i,j}$ 表示 $(i,j)$ 的权值。 时间复杂度 动态规划的时间复杂度是 $O(n^3)$。 C++ 代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值