[LOJ#2330]「清华集训 2017」榕树之心

本文通过生动的故事背景介绍了一个有趣的算法问题——榕树之心。问题要求确定在一棵不断生长的榕树模型中,哪些节点可能是榕树心最终停留的位置。文章详细解释了解决方案,包括如何通过递归算法计算每个节点作为终点的可能性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

[LOJ#2330]「清华集训 2017」榕树之心

试题描述
深秋。冷风吹散了最后一丝夏日的暑气,也吹落了榕树脚下灌木丛的叶子。相识数年的Evan和Lyra再次回到了小时候见面的茂盛榕树之下。小溪依旧,石桥依旧,榕树虽是历经荣枯更迭,依旧亭亭如盖,只是Evan和Lyra再也不是七八年前不经世事的少年了。

……

“已经快是严冬了,榕树的叶子还没落呢……”

“榕树是常绿树,是看不到明显的落叶季节的……”

“唉……想不到已经七年了呢。榕树还是当年的榕树,你却不是当年的你了……”

“其实又有什么是一成不变的呢,榕树常绿,翠绿树冠的宏观永恒,是由无数细小树叶的荣枯更迭组成的。在时间的流逝中一切都在不断变化着呢……”

“但你看这榕树,日日如此,季季如此,年年如此,仿佛亘古不变般,盘根错节,郁郁葱葱。我在想,或许成为一棵树更好吧,任时间从枝叶间流过,我只守这一片绿荫就好。”

“榕树固然长久,但在这无限的时光里,终归是要湮灭于尘土的。与其像榕树一般,植根于一方泥土中感受年复一年的四季更替。倒不如在有限的时间里看过尽可能多的世界吧。再说了,榕树虽生长缓慢,却依旧会在每年春天抽出一根新的枝条去向外探索的呢……”

“真的吗,榕树在她漫长的一生里,就是这样往外一步步探索的吗?”

“毕竟就算树冠看起来一成不变,榕树也会随着时间周期变化,春天到了自然就是生长的时候了,她也应当做出对应的表现吧……”

“相比于对季节更替做出本能的生长,我倒宁愿相信,榕树有一颗活跃的的,探索的心。”

“其实榕树是有心的,榕树刚刚种下的时候,心就在根的地方发芽了。以后每年春天榕树长出新枝条的时候,心就会向着新枝条的方向移动一点,这样就能更靠近外面的世界了。你看这头顶上的枝条,纵横交错,其实心已经在这枝杈间,移动了数十载了呢……”

“哇,也就是说,这密密麻麻的树杈中的某个地方,藏着这棵榕树的心吗?”

“没错,可是要知道它在哪,就得另花一番功夫了……”

“呀,这时候想想,一株树还是不如一个人好……比如你,要是这样贴上去的话,就能听到跳动的声音呢……”

……

一棵榕树可以抽象成一棵 \(n\) 个结点的有根树,其中结点编号为 \(1 \sim n\),而 \(1\) 号点就是根节点。初始时,树只有一号点,而心也在一号点。之后每一步,树都会长出一个新结点,即某个和当前已经存在的某个结点相邻的结点被加入了树中,之后,心会沿着心到新加结点的简单路径移动一步。这棵 \(n\) 个结点的树有很多种生长的顺序,不同的顺序可能会导致最终心的位置不同。现在,Evan和Lyra想知道,哪些结点可能是心在生长过程结束时停留的位置呢?

例如一棵大小为 \(4\) 的树,连边为 \(\{<1,2>,<1,3>,<1,4>\}\),我们有三种不同的生长顺序可以让心分别停留在 \(2,3,4\) 号节点上:

最终停留在 \(2\) 号点:

  1. \(1\) 生长出 \(3\),心从 \(1\) 移动到 \(3\),
  2. \(1\) 生长出 \(4\),心从 \(3\) 移动回 \(1\),
  3. \(1\) 生长出 \(2\),心从 \(1\) 移动到 \(2\).

最终停留在 \(3\) 号点:

  1. \(1\) 生长出 \(2\),心从 \(1\) 移动到 \(2\),
  2. \(1\) 生长出 \(4\),心从 \(2\) 移动回 \(1\),
  3. \(1\) 生长出 \(3\),心从 \(1\) 移动到 \(3\).

最终停留在 \(4\) 号点:

  1. \(1\) 生长出 \(2\),心从 \(1\) 移动到 \(2\),
  2. \(1\) 生长出 \(3\),心从 \(2\) 移动回 \(1\),
  3. \(1\) 生长出 \(4\),心从 \(1\) 移动到 \(4\).

而我们可以证明,不存在任何一种可能的生长顺序使得心停留在 \(1\) 号点。

输入

从标准输入读入数据。

输入第一行一个两个正整数 \(W, T\),分别表示子任务编号(在样例中 \(W=0\))和数据组数,接下来是 \(T\) 组数据的描述,对于每组数据:

第一行一个正整数 \(n\) 表示树上结点的个数。

接下来 \(n-1\) 行,每行两个正整数 \(ia_i,b_i\),表示编号 \(a_i,b_i\) 的结点间有一条树边,保证 \(a_i \neq b_i\) 并且输入的 \(n-1\) 条边恰好构成了一棵树。

输出

输出到标准输出。

若输入的 \(W\) 不等于 \(3\),对于每组数据输出一行一个长度为 \(n\)\(01\) 字符串,表示编号为 \(1 \sim n\) 的结点是否有可能是心最后所在的位置,若 \(01\) 字符串对应位是 \(1\) 则表示可能,为 \(0\) 则表示不可能。

若输入的 \(W\) 等于 \(3\),则对每组数据输出一个字符表示 \(1\) 号点的答案。

输入示例
0 3
4
1 2
1 3
1 4
6
1 2
1 3
1 4
4 5
5 6
10
1 2
1 3
3 4
3 5
3 6
4 7
7 8
8 9
9 10
输出示例
0111
000101
0000001010
数据规模及约定

Subtask 1[10pts]

\(T \leq 50; n \leq 15\)

Subtask 2[10pts]

\(T \leq 20; n \leq 10^5\)。 除了 \(1\) 号点之外,每个点度数(包括父亲)不超过 \(2\)

Subtask 3[10pts]

\(T \leq 200; n \leq 100\)。 只输出一个字符表示 \(1\) 号点答案,即保证 \(1\) 号点答案正确即可。

Subtask 4[35pts]

\(T \leq 20; n \leq 10^3\)

Subtask 5[35pts]

\(T \leq 20; n \leq 10^5\)

题解

考虑“生长”效果可以互相消去。先考虑只用判断能否到达根节点的子任务,我们发现当且仅当所有生长效果可以被消干净时才有可能最后停到根节点。

然后对于所有子树,最难消的肯定是最大的那颗子树,并且可以直观感觉到,最大的子树越小,越容易消干净,那么怎么量化这个感觉呢?

我们不妨设 \(elim(i)\) 表示对于子树 \(i\),它最多能消掉的数,令 \(siz_i\) 表示子树 \(i\) 中的节点数,\(s_m\)\(i\) 最大的儿子。

那么当 \(siz_{s_m} \le siz_i - 1 - siz_{s_m}\) 时,可以用其他点把子树 \(s_m\) 全部消掉,如果其他点多出来了,它们之间可以互相消掉,唯一可能剩下的情况就是 \(siz_i - 1\) 为奇数,那么肯定会剩下一个节点。

\(siz_{s_m} > siz_i - 1 - siz_{s_m}\) 时,就考虑先让儿子 \(s_m\) 自己消,消到一定时候我们用兄弟助他一臂之力,看能不能消光;即 \(2 \cdot elim(s_m) + siz_i - 1 - siz_{s_m} \ge siz_{s_m}\) 时,能够全消光(奇数的话剩一个),否则能消的对数就是 \(elim(s_m) + siz_i - 1 - siz_{s_m}\)

那么再考虑满分做法,对于节点 \(x\),如果最终能够到达,相当于把 \(1 \sim x\) 的路径缩成新的根,然后其他的子树互相消就好了。这样我们就可以 dfs 的同时维护需要的信息(\(elim(i)\)\(\mathrm{max}\{siz_i\}\) 等,这些信息可以很容易地合并,所以能够维护),然后遍历到的点就可以判断是否能够到达了。

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cctype>
#include <algorithm>
using namespace std;
#define rep(i, s, t) for(int i = (s); i <= (t); i++)
#define dwn(i, s, t) for(int i = (s); i >= (t); i--)

int read() {
    int x = 0, f = 1; char c = getchar();
    while(!isdigit(c)){ if(c == '-') f = -1; c = getchar(); }
    while(isdigit(c)){ x = x * 10 + c - '0'; c = getchar(); }
    return x * f;
}

#define maxn 100010
#define maxm 200010

int n, m, head[maxn], nxt[maxm], to[maxm];

void AddEdge(int a, int b) {
    to[++m] = b; nxt[m] = head[a]; head[a] = m;
    swap(a, b);
    to[++m] = b; nxt[m] = head[a]; head[a] = m;
    return ;
}

int siz[maxn], elim[maxn], ms[maxn], ms2[maxn];
void getElim(int u, int fa) {
    siz[u] = 1;
    ms[u] = ms2[u] = -1;
    for(int e = head[u]; e; e = nxt[e]) if(to[e] != fa) {
        getElim(to[e], u);
        siz[u] += siz[to[e]];
        if(ms[u] < 0) ms[u] = to[e];
        else if(siz[to[e]] > siz[ms[u]]) ms2[u] = ms[u], ms[u] = to[e];
        else if(ms2[u] < 0 || siz[to[e]] > siz[ms2[u]]) ms2[u] = to[e];
    }
    int others = 0;
    for(int e = head[u]; e; e = nxt[e]) if(to[e] != fa && to[e] != ms[u])
        others += siz[to[e]];
    if(others >= siz[ms[u]] - (elim[ms[u]] << 1)) elim[u] = siz[u] - 1 >> 1;
    else elim[u] = elim[ms[u]] + others;
    return ;
}

bool can[maxn];
void getCan(int u, int fa, int nsiz, int nelim, int nothers) {
    if(ms[u] < 0) {
//      printf("leave %d: %d %d %d\n", u, nsiz, nelim, nothers);
        if(nothers >= nsiz - (nelim << 1) && !(nsiz + nothers & 1)) can[u] = 1;
        else can[u] = 0;
        return ;
    }
    
    int siza, ela, others = nothers + siz[u] - siz[ms[u]] - 1;
    if(siz[ms[u]] > nsiz) siza = siz[ms[u]], ela = elim[ms[u]], others += nsiz;
    else siza = nsiz, ela = nelim, others += siz[ms[u]];
//  printf("node %d: %d %d %d\n", u, siza, ela, others);
    if(others >= siza - (ela << 1) && !(siza + others & 1)) can[u] = 1;
    else can[u] = 0;
    
    for(int e = head[u]; e; e = nxt[e]) if(to[e] != fa) {
        int nxsiz, nxelim, nxothers = nothers;
        if(to[e] == ms[u]) {
            nxothers += siz[u] - siz[to[e]] - 1 - (ms2[u] < 0 ? 0 : siz[ms2[u]]);
            if(ms2[u] < 0 || nsiz > siz[ms2[u]]) nxsiz = nsiz, nxelim = nelim, nxothers += siz[ms2[u]];
            else nxsiz = siz[ms2[u]], nxelim = elim[ms2[u]], nxothers += nsiz;
            getCan(to[e], u, nxsiz, nxelim, nxothers);
        }
        else {
            nxothers += siz[u] - siz[to[e]] - 1 - siz[ms[u]];
            if(nsiz > siz[ms[u]]) nxsiz = nsiz, nxelim = nelim, nxothers += siz[ms[u]];
            else nxsiz = siz[ms[u]], nxelim = elim[ms[u]], nxothers += nsiz;
            getCan(to[e], u, nxsiz, nxelim, nxothers);
        }
    }
    return ;
}

int main() {
    int W = read(), T = read();
    while(T--) {
        n = read();
        m = 0; memset(head, 0, sizeof(head));
        rep(i, 1, n - 1) {
            int a = read(), b = read();
            AddEdge(a, b);
        }
        memset(elim, 0, sizeof(elim));
        getElim(1, 0);
//      rep(i, 1, n) printf("%d%c", elim[i], i < n ? ' ' : '\n');
        getCan(1, 0, 0, 0, 0);
        if(W != 3){ rep(i, 1, n) putchar((int)can[i] + '0'); putchar('\n'); }
        else{ putchar((int)can[1] + '0'); putchar('\n'); }
    }
    
    return 0;
}

转载于:https://www.cnblogs.com/xiao-ju-ruo-xjr/p/8058610.html

内容概要:本文深入解析了扣子COZE AI编程及其详细应用代码案例,旨在帮助读者理解新一代低门槛智能体开发范式。文章从五个维度展开:关键概念、核技巧、典型应用场景、详细代码案例分析以及未来发展趋势。首先介绍了扣子COZE的核概念,如Bot、Workflow、Plugin、Memory和Knowledge。接着分享了意图识别、函数调用链、动态Prompt、渐进式发布及监控可观测等核技巧。然后列举了企业内部智能客服、电商导购助手、教育领域AI助教和金融行业合规质检等应用场景。最后,通过构建“会议纪要智能助手”的详细代码案例,展示了从需求描述、技术方案、Workflow节点拆解到调试与上线的全过程,并展望了多智能体协作、本地私有部署、Agent2Agent协议、边缘计算插件和实时RAG等未来发展方向。; 适合人群:对AI编程感兴趣的开发者,尤其是希望快速落地AI产品的技术人员。; 使用场景及目标:①学习如何使用扣子COZE构建生产级智能体;②掌握智能体实例、自动化流程、扩展能力和知识库的使用方法;③通过实际案例理解如何实现会议纪要智能助手的功能,包括触发器设置、下载节点、LLM节点Prompt设计、Code节点处理和邮件节点配置。; 阅读建议:本文不仅提供了理论知识,还包含了详细的代码案例,建议读者结合实际业务需求进行实践,逐步掌握扣子COZE的各项功能,并关注其未来的发展趋势。
06-21
<think>我们正在查询与LOJ6279相关的编程题目或解决方案。LOJ(LibreOJ)是一个在线的评测系统,主要收录算法竞赛题目。根据题号6279,我们需要确定该题目的具体内容。由于我无法直接访问网络,我将基于已知信息进行推理。在算法竞赛中,LOJ的题目编号通常与特定的题目对应。题号6279可能对应一个特定的问题。根据常见的题目类型,我们可以推测它可能涉及数据结构或算法。然而,为了准确回答,我们可以尝试回忆或推断常见的LOJ题目。例如,LOJ6000系列题目多为数据结构题,如线段树、树状数组等。实际上,通过搜索记忆,我回起LOJ6279是“数列分块入门3”(BlockArray3)题目。这是“分块”系列题目中的一题。题目描述:给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值x的前驱(即最大的小于x的数)。因此,用户需要的是关于LOJ6279题目的描述和解决方案。解决方案:由于题目要求分块处理,我们将数列分成若干块,每块大小为sqrt(n)。对于每个块,我们维护一个有序的数组(用于二分查找)以及一个加法标记(记录整个块被加了多少)。操作:1.区间加法:对于整块,直接更新加法标记;对于不完整的块,暴力更新元素值,然后重新排序该块(或者重新构建有序数组)。2.查询区间内小于x的前驱:遍历区间内的所有块。对于整块,在块的有序数组中二分查找小于(x-加法标记)的最大值;对于不完整的块,暴力遍历每个元素,判断是否小于x(注意加上加法标记)并更新最大值。代码实现(C++)示例:```cpp#include<iostream>#include<vector>#include<cmath>#include<algorithm>#include<climits>usingnamespacestd;constintMAXN=100010;constintBLOCK_SIZE=320;//约sqrt(MAXN)intn;vector<int>a;vector<int>block_add;//每块的加法标记vector<vector<int>>sorted_blocks;//每块的有序数组voidinit(){intblock_num=(n+BLOCK_SIZE-1)/BLOCK_SIZE;block_add.assign(block_num,0);sorted_blocks.resize(block_num);for(inti=0;i<n;i++){sorted_blocks[i/BLOCK_SIZE].push_back(a[i]);}for(inti=0;i<block_num;i++){sort(sorted_blocks[i].begin(),sorted_blocks[i].end());}}voidupdate_block(intblock_id){sorted_blocks[block_id].clear();intstart=block_id*BLOCK_SIZE;intend=min((block_id+1)*BLOCK_SIZE,n);for(inti=start;i<end;i++){sorted_blocks[block_id].push_back(a[i]);}sort(sorted_blocks[block_id].begin(),sorted_blocks[block_id].end());}voidadd(intl,intr,intc){intblock_l=l/BLOCK_SIZE,block_r=r/BLOCK_SIZE;if(block_l==block_r){for(inti=l;i<=r;i++){a[i]+=c;}update_block(block_l);}else{for(inti=l;i<(block_l+1)*BLOCK_SIZE;i++){a[i]+=c;}update_block(block_l);for(inti=block_l+1;i<block_r;i++){block_add[i]+=c;}for(inti=block_r*BLOCK_SIZE;i<=r;i++){a[i]+=c;}update_block(block_r);}}intquery(intl,intr,intc){intblock_l=l/BLOCK_SIZE,block_r=r/BLOCK_SIZE;intans=-1;if(block_l==block_r){for(inti=l;i<=r;i++){intval=a[i]+block_add[block_l];if(val<c){if(val>ans)ans=val;}}}else{for(inti=l;i<(block_l+1)*BLOCK_SIZE;i++){intval=a[i]+block_add[block_l];if(val<c){if(val>ans)ans=val;}}for(inti=block_l+1;i<block_r;i++){//在块i的有序数组中二分查找小于c-block_add[i]的最大值inttarget=c-block_add[i];autoit=lower_bound(sorted_blocks[i].begin(),sorted_blocks[i].end(),target);if(it!=sorted_blocks[i].begin()){it--;intval=*it+block_add[i];if(val>ans)ans=val;}}for(inti=block_r*BLOCK_SIZE;i<=r;i++){intval=a[i]+block_add[block_r];if(val<c){if(val>ans)ans=val;}}}returnans;}intmain(){cin>>n;a.resize(n);for(inti=0;i<n;i++){cin>>a[i];}init();for(inti=0;i<n;i++){intop,l,r,c;cin>>op>>l>>r>>c;l--;r--;//如果题目输入是1-indexed,则转换为0-indexedif(op==0){add(l,r,c);}else{cout<<query(l,r,c)<<endl;}}return0;}```注意:上述代码是示例代码,可能需要根据题目具体输入要求进行调整。§§相关问题§§1.LOJ6279题目的具体描述是什么?2.分块算法的时间复杂度是多少?3.除了分块,还有哪些方法可以解决LOJ6279?4.如何优化分块算法的常数?5.分块算法适用于哪些场景?</think>### LOJ 6279 题目分析与解决方案 **题目描述**:LOJ 6279 是 LibreOJ 上的 **"数列分块入门 3"**,属于分块算法的经典练习题。核要求如下: - **操作 1**:区间 $[l, r]$ 内每个数加上值 $c$ - **操作 2**:查询区间 $[l, r]$ 内小于 $c$ 的**最大元素值**(即前驱),不存在时返回 $-1$ #### 分块算法设计 分块的核是将数列划分为 $\sqrt{n}$ 大小的块,平衡暴力操作与批量处理。 ##### 关键数据结构 1. **块内有序数组**: - 每块维护一个排序后的数组,用于二分查找前驱。 - 示例:块 $B_i$ 的排序数组为 $S_i$。 2. **加法懒标记**: - 记录整块被加的总值 $add_i$(不影响块内有序性)。 ##### 操作实现 1. **区间加法($[l, r] + c$)**: - **整块**:直接更新懒标记 $add_i \gets add_i + c$,时间复杂度 $O(1)$。 - **碎块**:暴力修改元素值,并**重构有序数组**(排序),时间复杂度 $O(\sqrt{n} \log \sqrt{n})$。 2. **查询前驱($[l, r]$ 内 $< c$ 的最大值)**: - **整块**:在有序数组 $S_i$ 中二分查找 $c - add_i$ 的前驱,时间复杂度 $O(\log \sqrt{n})$。 - **碎块**:暴力遍历元素,检查 $a_j + add_i < c$ 并更新最大值,时间复杂度 $O(\sqrt{n})$。 #### 代码实现(C++) ```cpp #include <iostream> #include <vector> #include <cmath> #include <algorithm> #include <climits> using namespace std; const int MAXN = 100010; int n, block_size; vector<int> a; vector<int> add_tag; // 块的加法标记 vector<vector<int>> blocks; // 每块的有序数组 void init() { block_size = sqrt(n); int block_num = (n + block_size - 1) / block_size; add_tag.assign(block_num, 0); blocks.resize(block_num); for (int i = 0; i < n; i++) blocks[i / block_size].push_back(a[i]); for (int i = 0; i < block_num; i++) sort(blocks[i].begin(), blocks[i].end()); } void rebuild_block(int block_id) { blocks[block_id].clear(); int start = block_id * block_size; int end = min(start + block_size, n); for (int i = start; i < end; i++) blocks[block_id].push_back(a[i]); sort(blocks[block_id].begin(), blocks[block_id].end()); } void range_add(int l, int r, int c) { int block_l = l / block_size, block_r = r / block_size; if (block_l == block_r) { // 同一块内 for (int i = l; i <= r; i++) a[i] += c; rebuild_block(block_l); } else { // 左侧碎块 for (int i = l; i < (block_l + 1) * block_size; i++) a[i] += c; rebuild_block(block_l); // 中间整块 for (int i = block_l + 1; i < block_r; i++) add_tag[i] += c; // 右侧碎块 for (int i = block_r * block_size; i <= r; i++) a[i] += c; rebuild_block(block_r); } } int query_predecessor(int l, int r, int c) { int ans = -1; int block_l = l / block_size, block_r = r / block_size; // 左侧碎块暴力查询 for (int i = l; i < min(r + 1, (block_l + 1) * block_size); i++) { int val = a[i] + add_tag[block_l]; if (val < c && val > ans) ans = val; } // 中间整块二分查找 for (int i = block_l + 1; i < block_r; i++) { int target = c - add_tag[i]; auto it = lower_bound(blocks[i].begin(), blocks[i].end(), target); if (it != blocks[i].begin()) { it--; int val = *it + add_tag[i]; if (val < c && val > ans) ans = val; } } // 右侧碎块暴力查询 if (block_l != block_r) { for (int i = block_r * block_size; i <= r; i++) { int val = a[i] + add_tag[block_r]; if (val < c && val > ans) ans = val; } } return ans; } int main() { cin >> n; a.resize(n); for (int i = 0; i < n; i++) cin >> a[i]; init(); for (int i = 0; i < n; i++) { int op, l, r, c; cin >> op >> l >> r >> c; l--; r--; // 转为0-indexed if (op == 0) range_add(l, r, c); else cout << query_predecessor(l, r, c) << endl; } return 0; } ``` #### 算法分析 - **时间复杂度**: - 单次修改/查询:$O(\sqrt{n} \log \sqrt{n})$(碎块排序主导)。 - 总操作 $m$ 次:$O(m \sqrt{n} \log n)$。 - **空间复杂度**:$O(n)$。 #### 优化技巧 1. **减少排序次数**: - 碎块修改时只重构受影响块的有序数组。 2. **块大小调整**: - 实测调整块大小为 $n^{0.6}$ 可能更快(需测试)。 #### 应用场景 分块算法适用于**强制在线**的区间问题(如 LOJ 的数列分块系列题),在 $O(\sqrt{n})$ 复杂度下平衡修改与查询[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值