树上启发式合并学习笔记+杂题
图论系列:
前言:
欲买桂花同载酒,终不似,少年游。
相关题单:戳我
一.树上启发式合并
前置知识:树的重儿子。
1.引入
启发式算法是基于人类的经验和直观感觉,对一些算法的优化。(其实就是感觉是对的就是对的),例如并查集的启发式合并,将小集合合并到大集合中。
因为在路径压缩的时候,大集合的根没有改变,只有小集合内的元素会递归根,于是优化了时间复杂度。
2.过程
有点抽象啊,还是给定例题吧。
例题:树上数颜色
给出一棵
发现这题没法 dfs
一次直接做的原因?因为对于存颜色的桶只可能开一个,记
相当于为了得知一个点的答案,我们需要遍历这个点的整棵子树。这样时间复杂度就是
为了尽可能的优化时间复杂度,贪心的想,当然是保留子树最大的那个儿子,也就是重儿子。于是对于一个点,首先先遍历完其所有的轻儿子,对于轻儿子统计完答案便将其贡献删去,然后统计重儿子的答案,重儿子的贡献保留。
显然对于一个点,如果其遍历了
3.证明
类似树链剖分的证明,还是定义连向重儿子的为重边,连向轻儿子的为轻边,对于一棵有
根节点到树上任意节点的轻边数不超过
又因为如果一个节点是其父亲的重儿子,则它的子树必定在它的兄弟之中最多,所以任意节点到根的路径上所有重边连接的父节点在计算答案时必定不会遍历到这个节点,所以一个节点的被遍历的次数等于它到根节点路径上的轻边数
4.实现
例题的代码
const int M=2e5+5;
int n,maxson,num;
int c[M],t[M],ans[M];
int cnt=0;
struct N{
int to,next;
}; N p[M<<1];
int head[M],siz[M],son[M];
inline void add(int a,int b)
{
++cnt;
p[cnt].next=head[a];
head[a]=cnt;
p[cnt].to=b;
}
inline void dfs1(int u,int f)
{
siz[u]=1;
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f) continue;
dfs1(v,u);
siz[u]+=siz[v];
if(siz[v]>siz[son[u]]) son[u]=v;
}
}//就是树剖的第一个dfs,目的是找出每个点的重儿子
inline void solve(int u,int f,int k)
{
if(k>0)
{
if(++t[c[u]]==1) ++num;
}
else
{
if(--t[c[u]]==0) --num;
}
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f||v==maxson) continue;
solve(v,u,k);
}
}
inline void dfs(int u,int f,int opt)//opt为0表示这是轻儿子,否则是重儿子
{
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f||v==son[u]) continue;
dfs(v,u,0);//儿子之间颜色不能乱,先将轻儿子的答案统计出来,递归出来的时候由于opt=0所以贡献会删完
}
if(son[u]) dfs(son[u],u,1),maxson=son[u];//如果有重儿子就统计重儿子,同时记录这个点的重儿子
solve(u,f,1);//暴力加上轻儿子的答案(所以要将重儿子排除在外)
ans[u]=num,maxson=0;//注意,如果是轻儿子的话,轻儿子要删完
if(!opt) solve(u,f,-1);//是轻儿子,贡献删完
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++) cin>>c[i];
for(int i=1,a,b;i<n;i++)
{
cin>>a>>b;
add(a,b),add(b,a);
}
dfs1(1,0);
dfs(1,0,1);
for(int i=1;i<=n;i++) cout<<ans[i]<<" ";
return 0;
}
5.习题
CF208E Blood Cousins
一棵树/森林,给定
那么对于一个询问朴素应该怎么做?显然是先找到
那么分析一下这种问题是否可以启发式合并,主要是看是否儿子贡献是否可以传递给父亲啊。考虑暴力做法,显然就是对于每一个点跑其子树,统计出其子树内各个深度的点的个数即可。这样如果一个儿子统计完其子树内各个深度的节点个数,显然是可以转移给父亲的。
于是使用树上启发式合并,对于每个节点下的子树都暴力统计各个深度的节点数,保留重儿子,删去轻儿子的贡献。
代码:
const int M=1e5+5;
int n,q,maxson;
int fa[M][18],t[M],ans[M];
vector<int> r;
vector<pii> s[M];
int cnt=0;
struct N{
int to,next;
};N p[M<<1];
int head[M];
inline void add(int a,int b)
{
++cnt;
p[cnt].next=head[a];
head[a]=cnt;
p[cnt].to=b;
}
int siz[M],son[M],deep[M];
inline void dfs1(int u,int f)
{
deep[u]=deep[f]+1,siz[u]=1,fa[u][0]=f;
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f) continue;
dfs1(v,u);
siz[u]+=siz[v];
if(siz[v]>siz[son[u]]) son[u]=v;
}
}
inline void solve(int u,int f,int k)
{
t[deep[u]]+=k;//暴力统计各个深度的点的个数
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f||v==maxson) continue;
solve(v,u,k);
}
}
inline void dfs2(int u,int f,int opt)//opt=0还是表示为轻儿子,否则为重儿子
{
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==son[u]||v==f) continue;
dfs2(v,u,0);
}
if(son[u]) dfs2(son[u],u,1),maxson=son[u];//最后一个遍历重儿子,因为要保留贡献
solve(u,f,1),maxson=0;//又将轻儿子的贡献暴力加进来
for(auto it:s[u])//处理当前点上的询问
{
ans[it.second]=t[deep[u]+it.first]-1;
}
if(!opt) solve(u,f,-1);//如果是轻儿子就需要删去这个点子树下的所有贡献
}
inline int find(int x,int res)
{
for(int i=17;i>=0;--i)
{
if(res>=deep[x]-deep[fa[x][i]])
{
res-=(deep[x]-deep[fa[x][i]]);
x=fa[x][i];
}
}
if(res) return 0;
return x;
}//树上倍增
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1,x;i<=n;++i)
{
cin>>x;
if(!x) r.push_back(i);
else add(x,i);
}
for(int root:r) dfs1(root,0);//可能不连通ing,预处理出重儿子以及各个点的父亲节点,以及各个点的深度
for(int j=1;j<=17;++j)
{
for(int i=1;i<=n;++i) fa[i][j]=fa[fa[i][j-1]][j-1];
}//由于要跳某个点的k级祖先,所以使用树上倍增,这里是预处理
cin>>q;
for(int i=1,x,k,f;i<=q;++i)
{
cin>>x>>k;
f=find(x,k);//x的树上k级祖先(如果找到的f是0,说明这个点没有树上第k级祖先,答案自然为0,不用管)
if(f) s[f].push_back(mk(k,i));//将每个节点的询问存在一个vector中(一个点上可能包含了多个询问),要找的就是k级祖先子树内深度为k的节点个数,i表示这个是第几个询问,好将答案存下来
}
for(int root:r) dfs2(root,0,0);//树上启发式合并
for(int i=1;i<=q;++i) cout<<ans[i]<<" ";
return 0;
}
CF291E Tree-String Problem
树上跑 kmp,当练习题(其实是没写代码力)
CF600E Lomsat gelral
给定一颗有根树,每个节点都有一个颜色,
还是想儿子的贡献是否可以转移到父亲节点上并且是否能快速得到询问的答案,如果我们是统计每种颜色出现了多少次的话,虽说是可以将贡献转移至父亲节点,但是答案似乎处理起来有点麻烦。因为可能有多个颜色占据主导地位,那如果我们使用一个桶数组记录一下每一个颜色出现的次数,然后用一个
可能会问如果颜色出现增减的时候是否好维护,增加的时候显然,减少的时候都是一个子树贡献全部砍掉,于是
代码:
const int M=1e5+5;
int n,res,max_son,maxx;
int c[M],ans[M],t[M];
//res表示出现次数最多的颜色的编号和,maxx表示出现次数最多的
int cnt=0;
struct N{
int to,next;
};N p[M<<1];
int head[M];
inline void add(int a,int b)
{
++cnt;
p[cnt].next=head[a];
head[a]=cnt;
p[cnt].to=b;
}
int son[M],siz[M];
inline void dfs1(int u,int f)
{
siz[u]=1;
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f) continue;
dfs1(v,u);
siz[u]+=siz[v];
if(siz[v]>siz[son[u]]) son[u]=v;
}
}
inline void dfs3(int u,int f,int k)
{
t[c[u]]+=k;
if(t[c[u]]>maxx) maxx=t[c[u]],res=c[u];
else if(t[c[u]]==maxx) res+=c[u];
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f||v==max_son) continue;
dfs3(v,u,k);
}
}
inline void dfs2(int u,int f,int opt)
{
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f||v==son[u]) continue;
dfs2(v,u,0);
}
if(son[u]) dfs2(son[u],u,1),max_son=son[u];
dfs3(u,f,1);
max_son=0,ans[u]=res;
if(!opt) dfs3(u,f,-1),maxx=res=0;//这个时候maxx&res直接赋0即可,因为整个子树都删了
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;++i) cin>>c[i];
for(int i=1,a,b;i<n;++i)
{
cin>>a>>b;
add(a,b),add(b,a);
}
dfs1(1,0),dfs2(1,0,1);//预处理重儿子+树上启发式合并
for(int i=1;i<=n;++i) cout<<ans[i]<<" ";cout<<"\n";
return 0;
}
CF570D Tree Requests
给定一个以 a
-z
),每个点的深度定义为该节点到
需要让某一深度的节点上的字母重新排序之后构成一个回文串,也就是说最多只有一个字母出现的次数是奇数次。对于每个深度每种字母出现的次数,显然是可以由儿子转移到父亲的。于是树上启发式合并,用
对于每一个查询,将其离线到每一个点上,在 dfs
的过程中,如果当前点有一个对于深度为 YES
,否则为 NO
。
代码:
#define pii pair<int,int>
#define mk make_pair
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=5e5+5;
int n,q,maxson,res,flag;
int ans[M],t[M][26];
string str;
vector<pii> s[M];
int cnt=0;
struct N{
int to,next;
};N p[M<<1];
int head[M];
inline void add(int a,int b)
{
++cnt;
p[cnt].next=head[a];
head[a]=cnt;
p[cnt].to=b;
}
int siz[M],son[M],deep[M],maxx[M];
inline void dfs1(int u,int f)
{
deep[u]=deep[f]+1,siz[u]=1,maxx[u]=deep[u];
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f) continue;
dfs1(v,u);
siz[u]+=siz[v],maxx[u]=max(maxx[u],maxx[v]);
if(siz[v]>siz[son[u]]) son[u]=v;
}
}
inline void solve(int u,int f,int k)
{
t[deep[u]][str[u]-'a']+=k;//简单的增添贡献
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f||v==maxson) continue;
solve(v,u,k);
}
}
inline void dfs2(int u,int f,int opt)
{
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==son[u]||v==f) continue;
dfs2(v,u,0);//先处理轻儿子的答案
}
if(son[u]) dfs2(son[u],u,1),maxson=son[u];//然后处理重儿子的答案并保留
solve(u,f,1),maxson=0;
for(auto it:s[u])//处理当前点上的询问
{
res=0,flag=0;
//要求的深度是it.first
for(int i=0;i<26;++i)
{
if(t[it.first][i]&1) ++flag;
res+=t[it.first][i];
}
if(res&1)
{
if(flag==1) ans[it.second]=1;
}
else
{
if(!flag) ans[it.second]=1;
}
}
if(!opt) solve(u,f,-1);
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>q;
for(int i=2,x;i<=n;++i) cin>>x,add(x,i);
cin>>str,str=' '+str;
dfs1(1,0);//预处理重儿子
for(int i=1,x,k;i<=q;++i)
{
cin>>x>>k;
if(k<deep[x]||maxx[x]<k) ans[i]=1;//特判
else s[x].push_back(mk(k,i));//将其离线
}
dfs2(1,0,1);
for(int i=1;i<=q;++i) cout<<(ans[i]?"Yes":"No")<<"\n";
return 0;
}
CF1009F Dominant Indices
也是板子题,给定一棵以
考虑到对于一个点子树内的距离其为
所以问题又转化为了统计一个节点子树内出现次数最多的深度,这个显然比较简单,拿桶统计一下,然后在用一个变量
代码:
const int M=1e6+5;
int n,max_son,maxx,pos;
int t[M],ans[M];
int cnt=0;
struct N{
int to,next;
};N p[M<<1];
int head[M];
inline void add(int a,int b)
{
++cnt;
p[cnt].next=head[a];
head[a]=cnt;
p[cnt].to=b;
}
int siz[M],son[M],deep[M];
inline void dfs1(int u,int f)
{
siz[u]=1,deep[u]=deep[f]+1;
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f) continue;
dfs1(v,u);
siz[u]+=siz[v];
if(siz[v]>siz[son[u]]) son[u]=v;
}
}
inline void solve(int u,int f,int k)
{
t[deep[u]]+=k;
if(t[deep[u]]>maxx) maxx=t[deep[u]],pos=deep[u];
else if(t[deep[u]]==maxx) pos=min(pos,deep[u]);//处理pos的值
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f||v==max_son) continue;
solve(v,u,k);
}
}
inline void dfs2(int u,int f,int opt)
{
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f||v==son[u]) continue;
dfs2(v,u,0);
}
if(son[u]) dfs2(son[u],u,1),max_son=son[u];
solve(u,f,1);
max_son=0,ans[u]=pos-deep[u];
if(!opt) solve(u,f,-1),maxx=pos=0;//一样的,不用管删单点的情况,树上启发式合并要删都是将所有的贡献删完。
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1,a,b;i<n;++i)
{
cin>>a>>b;
add(a,b),add(b,a);
}
dfs1(1,0),dfs2(1,0,1);
for(int i=1;i<=n;++i) cout<<ans[i]<<"\n";
return 0;
}
CF375D Tree and Queries
给定一棵
比较经典,对于每个询问,还是离线到每个点头上。用桶
另外,这题还是树上莫队的模板题。
代码:
const int M=1e5+5;
int n,q,max_son;
int c[M],t[M],ans[M],pre[M];
vector<pii> s[M];
int cnt=0;
struct N{
int to,next;
};N p[M<<1];
int head[M];
inline void add(int a,int b)
{
++cnt;
p[cnt].next=head[a];
head[a]=cnt;
p[cnt].to=b;
}
int son[M],siz[M];
inline void dfs1(int u,int f)
{
siz[u]=1;
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f) continue;
dfs1(v,u);
siz[u]+=siz[v];
if(siz[v]>siz[son[u]]) son[u]=v;
}
}
inline void solve(int u,int f,int k)
{
if(k>0) ++t[c[u]],++pre[t[c[u]]];//修改t数组与pre数组
else --pre[t[c[u]]],--t[c[u]];
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f||v==max_son) continue;
solve(v,u,k);
}
}
inline void dfs2(int u,int f,int opt)
{
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f||v==son[u]) continue;
dfs2(v,u,0);
}
if(son[u]) dfs2(son[u],u,1),max_son=son[u];
solve(u,f,1);
for(auto it:s[u]) ans[it.second]=pre[it.first];
max_son=0;
if(!opt) solve(u,f,-1);
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>q;
for(int i=1;i<=n;++i) cin>>c[i];
for(int i=1,a,b;i<n;++i)
{
cin>>a>>b;
add(a,b),add(b,a);
}
for(int i=1,x,k;i<=q;++i)
{
cin>>x>>k;
s[x].push_back(mk(k,i));//将询问离线到每个点上
}
dfs1(1,0),dfs2(1,0,1);
for(int i=1;i<=q;++i) cout<<ans[i]<<"\n";
return 0;
}
CF246E Blood Cousins Return
给定一个森林,点有点权,多次询问某个节点子树内,距离其为某个值的所有点的点权拥有的不同的值。
也很简单喵,假设现在询问的是节点
由于点权是字符串,哈希完了之后不可能值域很小(毕竟如果有 map
或者 set
存就是了,这里用 map
啊,对于每一深度都拿一个 map
来记录该深度的点上的字符串,于是对于上面的答案自然就是 map
是支持删除一个键值的,非常方便。这样的时间复杂度是
代码:
const int M=2e5+5;
int n,q,maxson;
string str[M];
vector<pii> s[M];
vector<int> t;
map<string,int> mapp[M];
int cnt=0;
struct N{
int to,next;
};N p[M];
int head[M],ans[M];
inline void add(int a,int b)
{
++cnt;
p[cnt].next=head[a];
head[a]=cnt;
p[cnt].to=b;
}
int siz[M],son[M],deep[M];
inline void dfs1(int u,int f)
{
siz[u]=1,deep[u]=deep[f]+1;
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
dfs1(v,u);
siz[u]+=siz[v];
if(siz[v]>siz[son[u]]) son[u]=v;
}
}
inline void solve(int u,int f,int k)
{
if(k>0) mapp[deep[u]][str[u]]=1;//map直接统计
else mapp[deep[u]].erase(str[u]);//map直接删
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==maxson) continue;
solve(v,u,k);
}
}
inline void dfs2(int u,int f,int opt)
{
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==son[u]) continue;
dfs2(v,u,0);
}
if(son[u]) dfs2(son[u],u,1),maxson=son[u];
solve(u,f,1),maxson=0;
for(auto it:s[u])
{
ans[it.second]=mapp[it.first+deep[u]].size();//距离转深度
}
if(!opt) solve(u,f,-1);
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1,x;i<=n;++i)
{
cin>>str[i]>>x;
if(!x) t.push_back(i);
else add(x,i);
}
cin>>q;
for(int i=1,x,k;i<=q;++i)
{
cin>>x>>k;
s[x].push_back(mk(k,i));//询问离线至每个点
}
for(int root:t) dfs1(root,0);
for(int root:t) dfs2(root,0,0);
for(int i=1;i<=q;++i) cout<<ans[i]<<"\n";
return 0;
}
CF1585G Poachers
蛤,我还不会 SG 函数那快退吧 ,咕咕咕了(┭┮﹏┭┮)。
P9886 [ICPC2018 Qingdao R] Kawa Exam
咕一下,这题会补(坐等明早考试时补)。
CF741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths
神仙题,对于一棵根为 a
到 v
共
也是重新排序之后可以变成一个回文串啊,回文串就要求出现次数为奇数的字符最多为
-
可能是从
, 是子树内任意一点。 -
可能是
, 都是子树内任意一点。
现在要求一条满足要求的最长路径,我们发现,如果我们现在已经处理好了每个子树内的答案,那么相当于第二种路径可能,
那么如何记录一条路径的状态,观察到我们只关心某个字符出现次数的奇偶性,且字符集只有
为了方便,在一开始 dfs
的时候,将边权转为点权,然后做一遍异或前缀和,这样两个点权异或起来就是这两个点的路径异或权值了(因为原本是
代码:
const int M=5e5+5;
int n;
int ans[M],t[M*20],pre[25];
int cnt=0;
struct N{
int to,next,val;
};N p[M<<1];
int head[M];
inline void add(int a,int b,int c)
{
++cnt;
p[cnt].next=head[a];
head[a]=cnt;
p[cnt].to=b,p[cnt].val=c;
}
int siz[M],c[M],son[M],deep[M];
int dfn[M],id[M],num;
inline void dfs1(int u,int f)
{
dfn[u]=++num,id[num]=u,siz[u]=1,deep[u]=deep[f]+1;
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
c[v]=(c[u]^p[i].val);
dfs1(v,u);
siz[u]+=siz[v];
if(siz[v]>siz[son[u]]) son[u]=v;
}
}
inline void dfs2(int u,int f,int opt)
{
for(int i=head[u];i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f||v==son[u]) continue;
dfs2(v,u,0),ans[u]=max(ans[u],ans[v]);
}
if(son[u]) dfs2(son[u],u,1),ans[u]=max(ans[u],ans[son[u]]);//儿子们的答案继承
for(int k=0;k<=22;++k)
{
if(t[(c[u]^pre[k])]) ans[u]=max(ans[u],t[(c[u]^pre[k])]-deep[u]);//第一种路径
}
t[c[u]]=max(t[c[u]],deep[u]);
for(int i=head[u],pos;i!=0;i=p[i].next)
{
int v=p[i].to;
if(v==f||v==son[u]) continue;
for(int j=dfn[v];j<=dfn[v]+siz[v]-1;++j)
{
pos=id[j];//当前的这个点
for(int k=0;k<=22;++k)
{
if(t[(c[pos]^pre[k])])
{
ans[u]=max(ans[u],t[(c[pos]^pre[k])]+deep[pos]-2*deep[u]);
}
}
}//对于每一颗子树先统计对当前点的答案,然后更新t数组(每种路径的深度最深为多少)
for(int j=dfn[v];j<=dfn[v]+siz[v]-1;++j)//子树在dfs序中就是一段
{
pos=id[j];
t[c[pos]]=max(t[c[pos]],deep[pos]);
}
//这里为和不含重儿子?因为重儿子对t的贡献是保留了的(不然咋叫启发式合并)
//相当于每一个轻儿子都会和重儿子内的点连接一遍,保证所有路径都被统计到了
}
if(!opt)
{
for(int i=dfn[u],pos;i<=dfn[u]+siz[u]-1;++i) pos=id[i],t[c[pos]]=0;
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n,pre[1]=1;
for(int i=2;i<=22;++i) pre[i]=pre[i-1]*2;//预处理出合法路径的情况(那么一条路径异或上合法路径,找到的另一种路径权值和当前路径异或起来就是合法路径,即x^y=z,x^z=y)
char opt;
for(int i=2,x;i<=n;++i) cin>>x>>opt,add(x,i,(1<<(opt-'a')));//先将边权赋上
dfs1(1,0);//处理重儿子,边权转点权+异或前缀和
dfs2(1,0,1);//树上启发式合并
for(int i=1;i<=n;++i) cout<<ans[i]<<" ";
return 0;
}
CF715C Digit Tree
练习题,思路很简单,但是代码时间有点复杂。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效