Codeforces 1017G The Tree(分块DFS)

Codeforces1017G解析
本文介绍Codeforces1017G题目的解决思路,通过分块建立小树来减少时间复杂度,实现高效的查询处理。文章详细解释了算法步骤,并提供了完整的代码。

Codeforces 1017G The Tree

题目大意:

给一个一开始所有节点都是白色的树,给一些查询操作,给的三种操作:
1.在v的所有子节点中向下深搜,直到找到第一个白色子孙节点(或者自己),染成黑色。
2.把v的所有子树(包括它自己)全部染白。
3.输出v节点的颜色。

思路:

一般想到就是大模拟,直接暴力深搜Q次,操作3是O(1),其他都是O(n),时间复杂度总的就是。。。。O( nq n q ),Boom~
所以看了看官方题解,毕竟这个是相当于Div1 E题难度的题目,我这种接近Newbie警告的pupil,怎么可能会写。。。但是看过官方题解觉得好像不难啊,就决定花一个早上研究了一下写掉了,而且发现标程里面有些东西是不太必要的,比如那个存放原本状态的old_black数组。初始化我改用了memset,比for循环稍好,并且不像标程一样每一个V2数组都要重新初始化,我只初始化本轮查询需要用到的就可以了。然而。。。并没有什么卵用,总的思路和标程是一样一样的。
总的思路是对查询分块建立一棵小树,只存放被查询到的点,并记录被查询到的节点间白色块的数量,在小树上DFS进行三种操作,直到该块查询全部预处理完成后再将更新应用到整棵树上。总的时间复杂度一下子就降到了O( nn n n )级别
主要节省的时间在两个地方:
1.分块后,每块查询的时候,并不需要每次遍历每个节点,跳过了一些无关紧要的节点最后再更新。
2.有些重复的操作,预处理的时候就能避免掉,比如对同一个节点反复进行操作2的清空,或者一直间或染黑染白。
代码(带详细的注释(虽然图方(zhuang)便(bi)全都是英文)

#include <iostream>
#include <vector>
#include <cmath>
#include <cstring>
using namespace std;
struct mini//the data structure storing edges of the mini-tree
{
    int to;//the son vertex
    int white;//the number of white vertices between two vertices we need to operate in the mini-tree
    int dis;//the distance between two mini-tree vertices in the original tree, we need this to do the operation 2
};
int v[100005];//the vertex we need to operate
int t[100005];//the type of operation we do
vector<int> edge[100005];//the origin tree, which has no weight on its edges. So we just need to storage its end.
vector<mini> minitree[100005];// A mini Tree, with a weighted edge
bool vis[100005];//mark the vertex if we need to add the vertex to the mini-tree
bool cl[100005];//mark if the clear operation should be done
bool isblack[100005];//mark the vertex. isblack[i] is true when i-th vertex is black
int pusher[100005];//check how many operation 1s should be done to the son 
void builder(int now,int pre,int white,int dis)
{
    //cout<<now<<endl;
    if(vis[now])
    {
        if(pre!=0)
        {
            mini temp;
            temp.to=now;
            temp.white=white;
            temp.dis=dis;
            minitree[pre].push_back(temp);//add an edge to the mini-tree
        }
        for(int i=0; i<edge[now].size(); i++)
        {
            builder(edge[now][i],now,0,0);//clear the number of whites and the distance
        }
    }
    else
    {
        if(!isblack[now])
        {
            white++;//count the white vertices
        }
        for(int i=0; i<edge[now].size(); i++)
        {
            builder(edge[now][i],pre,white,dis+1);//bypass all other vertices but count the white ones between two key vertices
        }
    }
}
void op1(int now)
{
    if(!isblack[now])
    {
        isblack[now]=true;//if the vertex is still white, just paint it black and quit
        return;
    }
    else
    {
        pusher[now]++;//remember the blocks to be drawn but not now,save some time if clear op is found;
        for(int i=0; i<minitree[now].size(); i++)
        {
            if(pusher[now]>minitree[now][i].white)
            {
                minitree[now][i].white=0;//this doesn't matter, the white value won't be used any more this round (because both of the vertices are marked black,you may never go to line 62,where is the only line which need this value again.),but we know, in this situation, it's natural to think that no vertices between the father and the son are white
                op1(minitree[now][i].to);//if the operation 1 was done more than the white vertices between two key vertices, just do it for his son
            }
        }
    }
}

void op2(int now)
{
    isblack[now]=false;
    pusher[now]=0;//all the sons are marked white, so all the pushes we made were abandoned
    cl[now]=true;
    for(int i=0; i<minitree[now].size(); i++)
    {
        minitree[now][i].white=minitree[now][i].dis;//if the vertex should be marked black again ,you may found this is affecting line 62.
        op2(minitree[now][i].to);
    }
}
void push_down(int now,bool clr,int push)
{
    if(vis[now])
    {
        clr=cl[now];
        push=pusher[now];
    }
    else//the vertices in minitree has correct status already if there's no else, you will find if the direct father of the visited vertex who have done the operation 2 already  has a push number of 1, the vertex will be wrongly marked white
    {
        if(clr)//if the vertex was cleared before, we need to clear it first, because operation2's area is much bigger than the operation 1,and the vertex may be marked black again.
        {
            isblack[now]=false;
        }
        if(!isblack[now] && push>0)
        {
            isblack[now]=true;
            push--;
        }
    }
    for(int i=0; i<edge[now].size(); i++)
    {
        push_down(edge[now][i],clr,push);
    }
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n,q;
    cin>>n>>q;
    for(int i=2; i<=n; i++)
    {
        int temp;
        cin>>temp;
        edge[temp].push_back(i);
    }
    for(int i=1; i<=q; i++)
    {
        cin>>t[i]>>v[i];
    }
    memset(isblack,0,sizeof(isblack));//all the vertices were white before we do the operations
    int block=sqrt(n);
    for(int i=1; i<=q; i+=block)//Block the queries into pieces, one loop O(n+sqrt(n)+sqrt(n)+n),totally O(2(n+n*sqrt(n)))
    {

        memset(vis,0,sizeof(vis));//memset() is a little bit faster than the O(n) for loop, but still O(n)
        memset(cl,0,sizeof(cl));
        memset(pusher,0,sizeof(pusher));
        for(int j=0; j<block && i+j<=q; j++)//O(block),or O(sqrt(n))
        {
            minitree[v[i+j]].clear();//we just need to initialize the vectors we will use this time
            vis[v[i+j]]=true;

        }
        builder(1,0,0,0);//Build the mini tree with the original tree so that you can save a lot of time by bypassing useless vertices.This method is based on DFS, Traversal every vertex once, O(n)
        for(int j=0; j<block && i+j<=q; j++)
        {
            switch(t[i+j])
            {
            case 1:
                op1(v[i+j]);//DFS once in mini-tree, no more than O(block) or O(sqrt(n))
                break;
            case 2:
                op2(v[i+j]);//also DFS in mini-tree, O(block) or O(sqrt(n))
                break;
            case 3:
                if(isblack[v[i+j]])
                {
                    cout<<"black\n";
                }
                else
                {
                    cout<<"white\n";
                }
            }
        }
        push_down(1,0,0);// renew the tree,DFS in original tree,O(n)
    }

    return 0;
}

官方题解:https://codeforces.com/blog/entry/61081
题目链接:https://codeforces.com/contest/1017/problem/G

### Codeforces 平台上的整除分块题目及其解法 #### 1. 整除分块简介 整除分块是一种用于优化求和表达式的技巧,特别适用于处理形如 $\sum_{i=1}^{n}\left\lfloor \frac{n}{i} \right\rfloor$ 的问题。通过观察可以发现,在一定范围内,$\left\lfloor \frac{n}{i} \right\rfloor$ 是相同的,因此可以通过分段来减少重复计算。 #### 2. 应用实例:Codeforces 616E - Sum of Remainders 该题要求计算给定 $n$ 和 $m$ 下的 $\sum_{i=1}^{m}(n \% i)$[^3]。此题的核心在于如何高效地计算模运算的结果之和。利用整除分块的思想,可以将原问题转化为多个子区间内的快速求和操作: ```cpp #include <bits/stdc++.h> using namespace std; long long sum_of_remainder(long long n, long long m) { long long result = 0; for (int l = 1; l <= min(n, m); ) { int r = min(m / ((n / l)), m); if (!r) break; // Calculate the contribution from this block. result += (r - l + 1LL) * (n % l); l = r + 1; } return result; } ``` 上述代码实现了基于整除分块的方法来解决这个问题。具体来说,`l` 表示当前区间的左边界,而 `r` 则表示右边界;每次迭代都会找到一个新的有效区间 `[l,r]` 来加速计算过程。 #### 3. 另一应用案例:Polycarp and Div 3 虽然这道题表面上看起来与整除无关,但实际上也可以借助类似的思维模式来进行解答。题目描述了一个长度为 $n$、元素取值于 $[1,k]$ 的序列,并询问满足某些条件下的方案数量。这里的关键点在于理解 GCD 性质以及组合计数原理的应用[^1]。 为了简化复杂度高的暴力枚举方法,同样可以考虑采用类似于整除分块的方式——即针对不同因子构建对应的贡献矩阵并累加其结果。不过需要注意的是,这类转换通常会涉及到更复杂的数学变换和技术细节。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值