SamHH0912 さんのブログ

ブログ

#11589. Cave 题解

2025-07-10 22:44:17 By SamHH0912

题目传送门

题意简述

给定一棵 $n$ 个节点的,以 $1$ 为根的有根树,请按升序输出所有满足下面要求的正整数 $k$:

  • 这棵树可以被划分为 $k$ 个节点数目相同的连通块。

数据范围:$2\le n\le 3\times 10^6$。

解析

首先,我们需要找出可能成为连通块节点数目的所有正整数,记为 $d$。

显然,$d$ 必须是 $n$ 的因数。

如果将这部分的复杂度压缩至根号,不要忘记将数组排好序!! 我不会告诉你我为什么要提醒这一条

然后,降序枚举 $d$(对于 $1$ 和 $n$ 我们可以不经判断直接输出),判断树可否分为若干大小都为 $d$ 的连通块。

方法使用贪心,具体实现如下:

  • 对整棵树进行 DFS,同时维护布尔值 $Ans$(初始为 $1$)。对每个节点记录在其子树中与该节点位于同一连通块的节点数,保存为 $sz$ 数组。为表述方便,记每个连通块的大小为 $sz_0$(与当次的 $d$ 相等)。
  • 搜索到节点 $u$ 时,如果 $Ans=0$ 那么将 $sz_u$ 归零后返回(避免无用初始化)。
  • 如果 $Ans=1$,搜索 $u$ 的每个子节点 $v$。
  • 返回时:
    • 如果 $sz_u\gt sz_0$,那么将 $Ans$ 设为 $0$ 并返回;
    • 如果 $sz_u=sz_0$,那么直接返回;
    • 如果 $sz_u\lt sz_0$,那么将 $sz_{fa_u}$ 加上 $sz_u$ 并返回($fa_u$ 为 $u$ 的父亲)。
    • 无论哪种情况,返回前必须将 $sz_u$ 设为 $0$。
  • 整棵树搜完后,判断 $Ans$ 是否仍然为 $1$。若是,则 $\frac{n}{sz_0}$ 为答案。

接下来证明这种方法的正确性。

假设这是我们第一次割下来的,大小为 $sz_0$ 的块。如果我们不割它,那么显然没有别的解法。所以,有块,一定割。

再假设在一棵子树中能割的块都割完了,但剩下的大小大于 $sz_0$。显然,如果各个子节点还能割的话一定就割了,所以现在各个子节点的 $sz$ 值均小于 $sz_0$。我们不能抛下任意一棵子树(否则大小就不等于 $sz_0$ 了),所以此时一定无解。那么对于大小小于 $sz_0$ 的 $sz$ 值,我们只能选择把这个节点的剩余连通部分交给父节点去割。

所以每次验证 $d$ 是否可行都需要一次 $\mathcal{O}(n)$ 的 DFS。那么 $n$ 的正整数因数个数到底有多少呢?

这个答案有个相当宽松的上界,$2\sqrt{n}$。但在 $n\le 3\times 10^6$ 的条件下,较大的 $n$ 显然不可能达到这个值。事实上,题目范围内含有最多正整数因子的数为 $2\ 882\ 880$,含有 $336$ 个因子。而题目的时间限制是 10 秒,所以我们的算法肯定可以通过!(亲测,当 $n=10^3$ 时,$\mathcal{O}(n^3)$ 的区间 DP 关同步流甚至跑不到 500ms)

参考代码

#include <bits/stdc++.h>
using namespace std;
#define LL long long

#define _ 507
int d[_];//存n的因子

#define N 3000007

int cnt,head[N];
struct qwq{int v,nxt;}edge[N];//链式前向星存边
inline void AddE(int u,int v){
    edge[++cnt]={v,head[u]};
    head[u]=cnt;
}

bool Ans;//是否有解
int sz[N];//剩余块大小

inline void dfs(int u,int fa,int sz0){
    sz[u]++;//自己也得算
    for(int i=head[u];i;i=edge[i].nxt){
        int v=edge[i].v;
        dfs(v,u,sz0);
        if(!Ans) {sz[u]=0;return;}//记得归零
    }
    if(sz[u]>sz0) {sz[u]=0;Ans=0;return;}//记得归零
    if(sz[u]==sz0) {sz[u]=0;return;}//记得归零
    sz[fa]+=sz[u];sz[u]=0;//记得归零
}

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);

    int n,m=0;
    cin>>n;
    for(int i=2;i*i<=n;i++)//根号判因数
        if(n%i==0){
            d[++m]=i;
            if(i*i!=n) d[++m]=n/i;
        }
    sort(d+1,d+m+1);//如果根号判因数一定要记得sort!!
    for(int i=2,a;i<=n;i++) cin>>a,AddE(a,i);//父节点到子节点

    cout<<1<<' ';//显然
    for(int i=m;i;i--){
        sz[0]=0,Ans=1;dfs(1,0,d[i]);//d为连通块大小
        if(Ans) cout<<n/d[i]<<' ';//输出的为连通块个数
    }
    cout<<n<<'\n';//显然

    return 0;
}

感谢您的阅读!

コメント

No comments yet.

コメントを投稿

「@mike」を使うと mike さんに言及でき、mike さんの名前がハイライトされます。文字「@」を入力したい場合は「@@」と入力してください。

「/kel」と入力すると絵文字「kel」が使用できます。