树上启发式合并(DSU-on-Tree)

树上启发式合并DSU-on-Tree

  • 前言
  • 入门题
    • 例1
    • 例2
    • 例3CF600E
    • CF570D
  • 略复杂一点的题目
    • CF208E
    • NC235719
    • NC235715
  • 小结

前言

树上启发式合并,用来解决子树查询问题,是一种离线的、“暴力的”算法。从某种角度而言,与莫队算法有些类似,即:按照我们指定的顺序对数据做一个遍历,在遍历过程中记录询问的答案。CF上有一个专门的blog,讲了很多,本文只涉及到有关轻重链剖分的部分。oi-wiki上有一个更短的文章。

简单而言,就是对每一个节点,分两步:

  1. 递归统计子节点的数据
    • 先统计轻儿子(统计完的数据即刻清空)
    • 最后统计重儿子(统计完的数据保留下来)
  2. 统计自己的数据(就是再统计一遍轻儿子的数据,加上本节点的数据)

在统计本节点的数据时,因为重儿子的数据已经保存,因此只需要再统计一遍轻儿子的数据,再加上本节点的数据,就有了本节点子树的所有数据,于是可以回答有关本节点子树的相关询问。

可以证明(并不会证明),这样重复统计以及清空操作,数量级是 O ( N log ⁡ N ) O(N\log{N}) O(NlogN)的。

当然首先要会轻重链剖分,其实并不需要,因为这里其实并不需要实际剖出树链,只需要求出重儿子即可。因此实际上比剖分要简单的多。出于个人习惯,还是称之为剖分。

入门题

例1

为了简单起见,我们考虑一个不需要DSUonTree的问题,问子树的节点数量。这个显然 O ( N ) O(N) O(N)就能完成。我们考虑一下启发式合并。

#include <bits/stdc++.h>
using namespace std;struct dsu_on_tree_t{using vi = vector<int>;int N;
vector<vi> G; // 树, 1-indexstruct node_t{ // 树链剖分的结构int size; int hson; // 重儿子,这里是原树编号    int nid;  // 在树链剖分中的新编号int mdes; // 本子树全部在[nid, mdes]之间, 这是剖分编号
};
vector<node_t> Nodes;
vi New2Old;   // 剖分的编号为i,则原树节点编号为New2Old[i], 显然有Nodes[New2Old[i]].nid == i
int TimeStamp;
int Root;vector<int> Ans;
int Cnt; // 全局变量用于记录即时数据void init(int n){N = n;G.assign(N + 1, {});Nodes.assign(N + 1, {0, 0, 0, 0});New2Old.assign(N + 1, 0);TimeStamp = 0;Ans.assign(N + 1, 0);
}void mkDiEdge(int a, int b){G[a].push_back(b);
}void mkBiEdge(int a, int b){mkDiEdge(a, b);mkDiEdge(b, a);
}void dfsHeavy(int u, int p){ // 递归重儿子auto & n = Nodes[u];n.size = 1;New2Old[n.nid = ++TimeStamp] = u;for(auto v : G[u]){if(v == p) continue;dfsHeavy(v, u);n.size += Nodes[v].size;if(Nodes[n.hson].size < Nodes[v].size) n.hson = v;}n.mdes = TimeStamp;return;
}void dfs(int u, int p, bool keep){ // 递归const auto & n = Nodes[u];for(auto v : G[u]){if(v == p or v == n.hson) continue;dfs(v, u, false);}/// 最后递归重儿子if(n.hson) dfs(n.hson, u, true);/// 以下为统计u节点及其轻儿子if(n.hson){for(int i=n.nid;i<Nodes[n.hson].nid;++i) ++Cnt;/// 刚好把重儿子忽略掉for(int i=Nodes[n.hson].mdes+1;i<=n.mdes;++i) ++Cnt;}else{ // 只有一个节点++Cnt;}/// 此时可以回答问题Ans[u] = Cnt;/// 是否清空u子树对即时数据的影响if(not keep){for(int i=n.nid;i<=n.mdes;++i) --Cnt;}return;
}}Tree;int main(){
#ifndef ONLINE_JUDGEfreopen("z.txt", "r", stdin);
#endifios::sync_with_stdio(0); cin.tie(0); cout.tie(0);int n; cin >> n;Tree.init(n);for(int a,i=2;i<=n;++i){cin >> a;Tree.mkDiEdge(a, i);}Tree.dfsHeavy(1, 0);Tree.dfs(1, 0, true);for(int i=1;i<=n;++i) cout << Tree.Ans[i] << "\n";return 0;
}

首先关于树链剖分部分,加了一个数据域mdes,其作用主要是为了明确本子树的范围。这样方便后续将代码写成迭代实现。树链剖分的实现这里就略过了。

然后就是本文的核心实现函数dfs
这一部分就是递归,并且把重儿子放在最后递归。

    for(auto v : G[u]){if(v == p or v == n.hson) continue;dfs(v, u, false);}/// 最后递归重儿子if(n.hson) dfs(n.hson, u, true);

接下来就是统计即时数据,即统计u子树中除了u的重儿子之外的节点对即时数据造成的影响。因为记录了mdes,这一段可以写成迭代。迭代实现对于初次实现本算法的程序员,在调试方面比较友好。当然,也可以写成递归实现。

    /// 以下为统计u节点及其轻儿子if(n.hson){for(int i=n.nid;i<Nodes[n.hson].nid;++i) ++Cnt;/// 刚好把重儿子忽略掉for(int i=Nodes[n.hson].mdes+1;i<=n.mdes;++i) ++Cnt;}else{ // 只有一个节点++Cnt;}

接下来是根据当前的即时数据,回答跟u有关的问题。这里的话比较简单。

    /// 此时可以回答问题Ans[u] = Cnt;

最后是根据参数,决定是否要清空u子树对即时数据的影响。这里写成迭代就非常自然了。当然,递归也是极好的。

    /// 是否清空u子树对即时数据的影响if(not keep){for(int i=n.nid;i<=n.mdes;++i) --Cnt;}

对于DSU-on-Tree而言,这个流程是固定的。只需要考虑每个节点对即时数据造成的影响,以及如何根据即时数据回答问题即可。当然这两个动作需要能够“很快的”完成。

例2

再来看标准的例子,每个节点有一个颜色,问子树的不同颜色的种类的数量。只需要有一个计数器记录各种颜色的数量,并且记录计数器中不同种类的数量即可,都可以在 O ( 1 ) O(1) O(1)完成。因此加上DSU的流程,在 O ( N log ⁡ N ) O(N\log{N}) O(NlogN)可以完成。

#include <bits/stdc++.h>
using namespace std;struct dsu_on_tree_t{using vi = vector<int>;int N;
vector<vi> G; // 树, 1-indexstruct node_t{ // 树链剖分的结构int size; int hson; // 重儿子,这里是原树编号    int nid;  // 在树链剖分中的新编号int mdes; // 本子树全部在[nid, mdes]之间, 这是剖分编号
};
vector<node_t> Nodes;
vi New2Old;   // 剖分的编号为i,则原树节点编号为New2Old[i], 显然有Nodes[New2Old[i]].nid == i
int TimeStamp;
int Root;vector<int> Color;vector<int> Flag;
int Cnt; // 全局变量用于记录即时数据vector<int> Ans;
vector<vi> Questions;void init(int n){N = n;G.assign(N + 1, {});Nodes.assign(N + 1, {0, 0, 0, 0});New2Old.assign(N + 1, 0);TimeStamp = 0;Color.assign(N + 1, 0);Flag.assign(N + 1, Cnt = 0);
}void mkDiEdge(int a, int b){G[a].push_back(b);
}void mkBiEdge(int a, int b){mkDiEdge(a, b);mkDiEdge(b, a);
}void dfsHeavy(int u, int p){ // 递归重儿子auto & n = Nodes[u];n.size = 1;New2Old[n.nid = ++TimeStamp] = u;for(auto v : G[u]){if(v == p) continue;dfsHeavy(v, u);n.size += Nodes[v].size;if(Nodes[n.hson].size < Nodes[v].size) n.hson = v;}n.mdes = TimeStamp;return;
}void dfs(int u, int p, bool keep){ // 递归const auto & n = Nodes[u];for(auto v : G[u]){if(v == p or v == n.hson) continue;dfs(v, u, false);}/// 最后递归重儿子if(n.hson) dfs(n.hson, u, true);/// 以下为统计u节点及其轻儿子if(n.hson){for(int i=n.nid;i<Nodes[n.hson].nid;++i){if(1 == ++Flag[Color[New2Old[i]]]){++Cnt;}}for(int i=Nodes[n.hson].mdes+1;i<=n.mdes;++i){if(1 == ++Flag[Color[New2Old[i]]]){++Cnt;}}}else{ // 只有一个节点if(1 == ++Flag[Color[u]]){++Cnt;}}/// 此时可以回答问题for(auto i : Questions[u]){Ans[i] = Cnt;}/// 是否清空u子树对即时数据的影响if(not keep){for(int i=n.nid;i<=n.mdes;++i) {if(0 == --Flag[Color[New2Old[i]]]){--Cnt;}}}return;
}}Tree;int main(){
#ifndef ONLINE_JUDGEfreopen("z.txt", "r", stdin);
#endifios::sync_with_stdio(0); cin.tie(0); cout.tie(0);int n; cin >> n;Tree.init(n);for(int a,b,i=2;i<=n;++i){cin >> a >> b;Tree.mkBiEdge(a, b);}for(int i=1;i<=n;++i) cin >>Tree.Color[i];int q; cin >> q;Tree.Questions.assign(n + 1, {});Tree.Ans.assign(q, {});for(int a,i=0;i<q;++i){cin >> a;Tree.Questions[a].push_back(i);} Tree.dfsHeavy(1, 0);Tree.dfs(1, 0, true);for(auto i : Tree.Ans) cout << i << "\n";return 0;
}

NC23807的题目意思与本题是一模一样的,这里就不再赘述。

例3CF600E

这也是一个入门的例子,可以更好的认识清空操作的含义。给定树,每个节点有颜色。询问每一个子树的颜色的众数,即求出出现颜色最多的颜色编号;如果有多个颜色均出现最多次,则对编号求和。

本题显然也要弄一个计数器,然后每过一个节点,相应的颜色数量加加即可,对于众数或者众数求和都很简单。可能存在的一个问题在于如何清空?如果要考虑移除一个节点,对于答案的影响,那就很难办了。因为最值不支持快速的减法操作。但是注意到,本质上我们进行的是一个清空操作。考虑到递归顺序的安排,每次需要清空时,我们必然处在轻儿子处,且相应的重儿子必然在其之后。因此当本次清空完成时,实际上所有即时数据全部都清零了。之所以有些数据需要逐个节点、逐个节点的操作,是为了控制时间,只清除非零的数据。因为如果不管三七二十一,直接fill所有数据,显然是要超时的。

#include <bits/stdc++.h>
using namespace std;struct dsu_on_tree_t{using vi = vector<int>;int N;
vector<vi> G; // 树, 1-indexstruct node_t{ // 树链剖分的结构int size; int hson; // 重儿子,这里是原树编号    int nid;  // 在树链剖分中的新编号int mdes; // 本子树全部在[nid, mdes]之间, 这是剖分编号
};
vector<node_t> Nodes;
vi New2Old;   // 剖分的编号为i,则原树节点编号为New2Old[i], 显然有Nodes[New2Old[i]].nid == i
int TimeStamp;
int Root;vector<int> Color;vector<int> Flag;vector<long long> Ans;int MaxCnt;
long long NowAns;void init(int n){N = n;G.assign(N + 1, {});Nodes.assign(N + 1, {0, 0, 0, 0});New2Old.assign(N + 1, 0);TimeStamp = 0;Color.assign(N + 1, 0);Flag.assign(N + 1, MaxCnt = NowAns = 0);Ans.assign(N, 0);
}void mkDiEdge(int a, int b){G[a].push_back(b);
}void mkBiEdge(int a, int b){mkDiEdge(a, b);mkDiEdge(b, a);
}void dfsHeavy(int u, int p){ // 递归重儿子auto & n = Nodes[u];n.size = 1;New2Old[n.nid = ++TimeStamp] = u;for(auto v : G[u]){if(v == p) continue;dfsHeavy(v, u);n.size += Nodes[v].size;if(Nodes[n.hson].size < Nodes[v].size) n.hson = v;}n.mdes = TimeStamp;return;
}void dfs(int u, int p, bool keep){ // 递归const auto & n = Nodes[u];for(auto v : G[u]){if(v == p or v == n.hson) continue;dfs(v, u, false);}/// 最后递归重儿子if(n.hson) dfs(n.hson, u, true);/// 以下为统计u节点及其轻儿子if(n.hson){for(int i=n.nid;i<Nodes[n.hson].nid;++i){auto c = ++Flag[Color[New2Old[i]]];if(c > MaxCnt) MaxCnt = c, NowAns = Color[New2Old[i]];else if(c == MaxCnt) NowAns += Color[New2Old[i]];}for(int i=Nodes[n.hson].mdes+1;i<=n.mdes;++i){auto c = ++Flag[Color[New2Old[i]]];if(c > MaxCnt) MaxCnt = c, NowAns = Color[New2Old[i]];else if(c == MaxCnt) NowAns += Color[New2Old[i]];}}else{ // 只有一个节点Flag[NowAns = Color[u]] = MaxCnt = 1;}/// 此时可以回答问题Ans[u - 1] = NowAns;/// 是否清空u子树对即时数据的影响if(not keep){/// 注意这里是清空,因此可以直接令全局变量为零MaxCnt = NowAns = 0;for(int i=n.nid;i<=n.mdes;++i) {Flag[Color[New2Old[i]]] = 0;/// 一般习惯写成减减,但实际上直接清零也可以// --Flag[Color[New2Old[i]]];}}return;
}}Tree;int main(){
#ifndef ONLINE_JUDGEfreopen("z.txt", "r", stdin);
#endifios::sync_with_stdio(0); cin.tie(0); cout.tie(0);int n; cin >> n;Tree.init(n);for(int i=1;i<=n;++i) cin >>Tree.Color[i];for(int a,b,i=2;i<=n;++i){cin >> a >> b;Tree.mkBiEdge(a, b);}Tree.dfsHeavy(1, 0);Tree.dfs(1, 0, true);for(auto i : Tree.Ans) cout << i << " ";cout << endl;return 0;
}

这里统计答案的关键就在于清空的位置,一般习惯碰上计数器都是写减减,肯定不会有问题。但实际上直接把计数器清零也可以,因为这里其实不是清除单个节点对即时数据的影响,而是清空整个子树对即时数据的影响。又由于轻、重的递归顺序,因此清空轻儿子的效果实际上就是清空整个数据。因此这个操作才可以直接进行:MaxCnt = NowAns = 0;。同样的计数器数组也可以直接清零。之所以不用fill(Flag.begin(),Flag.end(),0)只是因为会超时。

CF570D

给定树,节点上有字符,有若干个询问(v, h),问子树v上深度为h的节点是否能够通过重排序构成回文串。注意节点深度是指整个树上的深度,因此是固定的。实际上就是问对应节点的字符数量,是否奇数个字母的数量少于等于1。由于只涉及到奇偶,可以使用一个26位二进制数表示数量,只需检查1的位数小于等于1即可。令cnt[d]表示表示深度为d的情况下的字符数量,使用DSU来维护cnt数组。然后对每个子树u,如果有关于它的问题,例如(u, h),则查询当时的cnt[h]、且记录下来即可。与前几题的区别,简而言之前几题使用了权值计数器,这里使用了深度计数器。

#include <bits/stdc++.h>
using namespace std;struct dsu_on_tree_t{using vi = vector<int>;int N;
vector<vi> G; // 树, 1-indexstruct node_t{ // 树链剖分的结构int size; int hson; // 重儿子,这里是原树编号    int nid;  // 在树链剖分中的新编号int mdes; // 本子树全部在[nid, mdes]之间, 这是剖分编号
};
vector<node_t> Nodes;
vi New2Old;   // 剖分的编号为i,则原树节点编号为New2Old[i], 显然有Nodes[New2Old[i]].nid == i
int TimeStamp;
int Root;
vector<int> Depth;string S;
vector<int> Ans;vector<vector<pair<int, int>>> Questions;vector<int> Cnt;void init(int n){N = n;G.assign(N + 1, {});Nodes.assign(N + 1, {0, 0, 0, 0});New2Old.assign(N + 1, 0);TimeStamp = 0;Depth.assign(N + 1, 0);Cnt.assign(N + 1, 0);
}void mkDiEdge(int a, int b){G[a].push_back(b);
}void mkBiEdge(int a, int b){mkDiEdge(a, b);mkDiEdge(b, a);
}void dfsHeavy(int u, int p){ // 递归重儿子Depth[u] = Depth[p] + 1;auto & n = Nodes[u];n.size = 1;New2Old[n.nid = ++TimeStamp] = u;for(auto v : G[u]){if(v == p) continue;dfsHeavy(v, u);n.size += Nodes[v].size;if(Nodes[n.hson].size < Nodes[v].size) n.hson = v;}n.mdes = TimeStamp;return;
}void dfs(int u, int p, bool keep){ // 递归const auto & n = Nodes[u];for(auto v : G[u]){if(v == p or v == n.hson) continue;dfs(v, u, false);}/// 最后递归重儿子if(n.hson) dfs(n.hson, u, true);/// 以下为统计u节点及其轻儿子if(n.hson){for(int i=n.nid;i<Nodes[n.hson].nid;++i){auto t = S[New2Old[i]] - 'a';Cnt[Depth[New2Old[i]]] ^= (1 << t);}for(int i=Nodes[n.hson].mdes+1;i<=n.mdes;++i){auto t = S[New2Old[i]] - 'a';Cnt[Depth[New2Old[i]]] ^= (1 << t);  }}else{ // 只有一个节点Cnt[Depth[u]] = 1 << (S[u] - 'a');}/// 此时可以回答问题for(const auto & pp : Questions[u]){auto h = pp.first;bool b = true;int c = 0;for(int i=0;i<26;++i){if((1 << i) & Cnt[h])if(++c > 1){b = false; break;}}Ans[pp.second] = b ? 1 : 0;}/// 是否清空u子树对即时数据的影响if(not keep){/// 清空for(int i=n.nid;i<=n.mdes;++i) {Cnt[Depth[New2Old[i]]] = 0;}}return;
}}Tree;int main(){
#ifndef ONLINE_JUDGEfreopen("z.txt", "r", stdin);
#endifios::sync_with_stdio(0); cin.tie(0); cout.tie(0);int n, q; cin >> n >> q;Tree.init(n);    for(int a,i=2;i<=n;++i){cin >> a;Tree.mkDiEdge(a, i);}cin >> Tree.S;Tree.S = " " + Tree.S;Tree.Questions.assign(n + 1, {});Tree.Ans.assign(q, 0);for(int u,h,i=0;i<q;++i){cin >> u >> h;Tree.Questions[u].emplace_back(h, i);}Tree.dfsHeavy(1, 0);Tree.dfs(1, 0, true);for(auto i : Tree.Ans) cout << (i ? "Yes\n" : "No\n");return 0;
}

CF246E是一个既有深度又有权值的题目,本质上就是询问子树u中深度为k的不同权值的数量。可以利用map作为计数器,在增加一个 log ⁡ \log log的情况下完成查询。

略复杂一点的题目

有了DSUonTree的基本流程,就可以把注意力集中在问题的转化与即时数据的快速维护上。只要能够在合理的时间复杂度内做到,就能解决整个问题。

CF208E

CF208E给定一个森林,定义父节点是子节点的一级祖先,类似可以定义k级祖先。如果两个节点u和v有一个共同的k级祖先,则它们互为k级兄弟。每次询问给定v和k,问v的k级兄弟有多少个。假设v的k级祖先是p,则等价于问p的k级子节点的数量。有了前面的关于深度相关计数器的经验,这个问题很容易解决。于是问题的关键变为了能否很快的知道任意节点的任意级祖先。这个显然是可以倍增解决的,与求 L C A LCA LCA类似。因此先做一个预处理,将问题转化一下,然后DSU即可。

#include <bits/stdc++.h>
using namespace std;int const LOGSZ = 17;struct dsu_on_tree_t{using vi = vector<int>;int N;
vector<vi> G; // 树, 1-indexstruct node_t{ // 树链剖分的结构int size; int hson; // 重儿子,这里是原树编号    int nid;  // 在树链剖分中的新编号int mdes; // 本子树全部在[nid, mdes]之间, 这是剖分编号
};
vector<node_t> Nodes;
vi New2Old;   // 剖分的编号为i,则原树节点编号为New2Old[i], 显然有Nodes[New2Old[i]].nid == i
int TimeStamp;
int Root;
vector<int> Depth;
vector<vi> Parent;
vector<int> Log2;vector<int> Ans;
vector<vector<pair<int, int>>> Questions;
vector<int> Cnt;void init(int n){N = n;G.assign(N + 1, {});Nodes.assign(N + 1, {0, 0, 0, 0});New2Old.assign(N + 1, 0);TimeStamp = 0;Depth.assign(N + 1, 0);Parent.assign(N + 1, vi(LOGSZ, 0));Cnt.assign(N + 1, 0);Log2.assign(N + 1, 0);for(int i=1;i<=N;++i){Log2[i] = Log2[i - 1] + ((1 << Log2[i - 1]) == i ? 1 : 0);}
}void mkDiEdge(int a, int b){G[a].push_back(b);
}void mkBiEdge(int a, int b){mkDiEdge(a, b);mkDiEdge(b, a);
}void dfsHeavy(int u, int p){ // 递归重儿子Depth[u] = Depth[p] + 1;auto & n = Nodes[u];n.size = 1;New2Old[n.nid = ++TimeStamp] = u;Parent[u][0] = p;for(int i=1;i<=Log2[Depth[u]];++i){Parent[u][i] = Parent[Parent[u][i - 1]][i - 1];}for(auto v : G[u]){if(v == p) continue;dfsHeavy(v, u);n.size += Nodes[v].size;if(Nodes[n.hson].size < Nodes[v].size) n.hson = v;}n.mdes = TimeStamp;return;
}void dfs(int u, int p, bool keep){ // 递归const auto & n = Nodes[u];for(auto v : G[u]){if(v == p or v == n.hson) continue;dfs(v, u, false);}/// 最后递归重儿子if(n.hson) dfs(n.hson, u, true);/// 以下为统计u节点及其轻儿子if(n.hson){for(int i=n.nid;i<Nodes[n.hson].nid;++i){++Cnt[Depth[New2Old[i]]];}for(int i=Nodes[n.hson].mdes+1;i<=n.mdes;++i){++Cnt[Depth[New2Old[i]]];}}else{ // 只有一个节点Cnt[Depth[u]] = 1;}/// 此时可以回答问题for(const auto & pp : Questions[u]){Ans[pp.second] = Cnt[pp.first + Depth[u]] - 1;}/// 是否清空u子树对即时数据的影响if(not keep){/// 清空for(int i=n.nid;i<=n.mdes;++i) {Cnt[Depth[New2Old[i]]] = 0;}}return;
}}Tree;vector<int> Root;int main(){
#ifndef ONLINE_JUDGEfreopen("z.txt", "r", stdin);
#endifios::sync_with_stdio(0); cin.tie(0); cout.tie(0);int n; cin >> n;Root.clear();Root.reserve(n);Tree.init(n);    for(int a,i=1;i<=n;++i){cin >> a;if(a) Tree.mkDiEdge(a, i);else Root.push_back(i);}for(auto i : Root) Tree.dfsHeavy(i, 0);int q; cin >> q;Tree.Questions.assign(n + 1, {});Tree.Ans.assign(q, 0);for(int p,u,k,i=0;i<q;++i){cin >> u >> k;auto target = k;for(int i=LOGSZ-1;i>=0;--i){if((1 << i) & target){u = Tree.Parent[u][i];}}Tree.Questions[u].emplace_back(k, i);}for(auto i : Root) Tree.dfs(i, 0, false);for(auto i : Tree.Ans) cout << (i) << " ";cout << endl;return 0;
}

NC235719

这个题目可以更好的展示DSUonTree的框架作用。每次回答询问[u,a,b],即u子树内深度在[a,b]区间内的所有节点的权值的最小值、最大值与和。这个明显是一个区间查询问题,再考虑到DSUonTree的统计过程,因此我们需要解决一个单点变动、区间查询的任务,因为要查询最值,所以使用线段树。如果不考虑线段树的部分,可以看到与DSUonTree有关的流程与之前的代码并无区别。

#include <bits/stdc++.h>
using namespace std;using llt = long long;
llt const INF = 0x1F2F3F4F5F6F7F8F;
llt const NINF = -INF;struct SegTree{ // 线段树带延迟using llt = long long;int N;using value_type = array<llt, 3>; // 最小值,最大值,和
vector<value_type> data; // 线段树using lazy_type = pair<llt, int>; 
vector<lazy_type> lazy; // 延迟标记/// 从下往上计算信息,要变动
value_type _up_(const value_type & ls, const value_type & rs) {// assert(0);return {min(ls[0], rs[0]), max(ls[1], rs[1]), ls[2] + rs[2]};
}/// 从上往下计算信息,要变动
void _dn_(int t, int s, int e, const lazy_type & delta) {// assert(0);if(delta.second == -1){ // 清零操作data[t] = value_zero();        }else if(delta.second == 1){ // 增加一个权值, 必然是单点操作data[t][0] = min(data[t][0], delta.first);data[t][1] = max(data[t][1], delta.first);data[t][2] += delta.first;}return;
}/// 初始化,清零,不用动
void init(int n) {data.assign((N = n) + 1 << 2, value_zero());// lazy.assign(n + 1 << 2, lazy_zero());
}/// 这个函数不用动
void modify(int a, int b, const lazy_type & delta){_modify(1, 1, N, a, b, delta);
}/// 这个函数不用动
value_type query(int a, int b){return _query(1, 1, N, a, b);
}/// 几乎不用动
value_type _query(int t, int s, int e, int a, int b) {if(a <= s and e <= b) {return data[t];}// _pushDown(t, s, e);int mid = (s + e) >> 1;value_type ans = value_zero(); // 如果求最值,这里不能用zeroif(a <= mid) ans = _up_(ans, _query(lson(t), s, mid, a, b));if(mid < b) ans = _up_(ans, _query(rson(t), mid + 1, e, a, b));return ans;
}/// 几乎不用动
void _modify(int t, int s, int e, int a, int b, const lazy_type & delta) {if(a <= s and e <= b) {_dn_(t, s, e, delta);return;}// _pushDown(t, s, e);int mid = (s + e) >> 1;if(a <= mid) _modify(lson(t), s, mid, a, b, delta);if(mid < b) _modify(rson(t), mid + 1, e, a, b, delta);_pushUp(t);return;
}/// 这个函数不用动
void _pushUp(int t) {data[t] = _up_(data[lson(t)], data[rson(t)]);
}/// 辅助函数,视延迟的类型而变动
static const lazy_type & lazy_zero() {static const lazy_type LAZY0 = {0, 0};return LAZY0; 
}/// 辅助函数,视线段树信息类型而变动
static const value_type & value_zero() {static const value_type VALUE0 = {INF, NINF, 0LL};return VALUE0;
}/// 这两个函数不用变动
static int lson(int x) {return x << 1;}
static int rson(int x) {return lson(x) | 1;}};struct dsu_on_tree_t{using vi = vector<int>;int N;
vector<vi> G; // 树, 1-indexstruct node_t{ // 树链剖分的结构int size; int hson; // 重儿子,这里是原树编号    int nid;  // 在树链剖分中的新编号int mdes; // 本子树全部在[nid, mdes]之间, 这是剖分编号
};
vector<node_t> Nodes;
vi New2Old;   // 剖分的编号为i,则原树节点编号为New2Old[i], 显然有Nodes[New2Old[i]].nid == i
int TimeStamp;
int Root;
vector<int> Depth;
SegTree St;
vector<long long> W;vector<array<llt, 3>> Ans;
vector<vector<array<int, 3>>> Questions;void init(int n){N = n;G.assign(N + 1, {});Nodes.assign(N + 1, {0, 0, 0, 0});New2Old.assign(N + 1, 0);TimeStamp = 0;Depth.assign(N + 1, 0);W.assign(N + 1, 0);St.init(N);
}void mkDiEdge(int a, int b){G[a].push_back(b);
}void mkBiEdge(int a, int b){mkDiEdge(a, b);mkDiEdge(b, a);
}void dfsHeavy(int u, int p){ // 递归重儿子Depth[u] = Depth[p] + 1;auto & n = Nodes[u];n.size = 1;New2Old[n.nid = ++TimeStamp] = u;for(auto v : G[u]){if(v == p) continue;dfsHeavy(v, u);n.size += Nodes[v].size;if(Nodes[n.hson].size < Nodes[v].size) n.hson = v;}n.mdes = TimeStamp;return;
}void dfs(int u, int p, bool keep){ // 递归const auto & n = Nodes[u];for(auto v : G[u]){if(v == p or v == n.hson) continue;dfs(v, u, false);}/// 最后递归重儿子if(n.hson) dfs(n.hson, u, true);/// 以下为统计u节点及其轻儿子if(n.hson){for(int i=n.nid;i<Nodes[n.hson].nid;++i){St.modify(Depth[New2Old[i]], Depth[New2Old[i]], {W[New2Old[i]], 1});}for(int i=Nodes[n.hson].mdes+1;i<=n.mdes;++i){St.modify(Depth[New2Old[i]], Depth[New2Old[i]], {W[New2Old[i]], 1});}}else{ // 只有一个节点St.modify(Depth[u], Depth[u], {W[u], 1});}/// 此时可以回答问题for(const auto & pp : Questions[u]){Ans[pp[2]] = St.query(Depth[u] + pp[0], Depth[u] + pp[1]);}/// 是否清空u子树对即时数据的影响if(not keep){/// 清空for(int i=n.nid;i<=n.mdes;++i){St.modify(Depth[New2Old[i]], Depth[New2Old[i]], {0LL, -1});}}return;
}}Tree;int main(){
#ifndef ONLINE_JUDGEfreopen("z.txt", "r", stdin);
#endifios::sync_with_stdio(0); cin.tie(0); cout.tie(0);int n; cin >> n;Tree.init(n);   for(int i=1;i<=n;++i) cin >> Tree.W[i];for(int a,b,i=1;i<n;++i){cin >> a >> b;Tree.mkBiEdge(a, b);}int q; cin >> q;Tree.Questions.assign(n + 1, {});Tree.Ans.assign(q, {});for(int u,a,b,i=0;i<q;++i){cin >> u >> a >> b;Tree.Questions[u].push_back({a, b, i});}Tree.dfsHeavy(1, 0);Tree.dfs(1, 0, true);for(const auto & i : Tree.Ans) cout << i[0] << " " << i[1] << " " << i[2] << "\n";return 0;
}

这道题还可以用来验证清空流程。上文说过对于需要清空的情况,实际上就是清空整个即时数据,之前之所以不这么做,只是因为超时。现在既然已经用上了线段树,可以再往里加点料,即支持延迟操作。这样的话直接清空整个线段树就没有压力了。

#include <bits/stdc++.h>
using namespace std;using llt = long long;
llt const INF = 0x1F2F3F4F5F6F7F8F;
llt const NINF = -INF;struct SegTree{ // 线段树带延迟using llt = long long;int N;using value_type = array<llt, 3>; // 最小值,最大值,和
vector<value_type> data; // 线段树using lazy_type = pair<llt, int>; 
vector<lazy_type> lazy; // 延迟标记/// 从下往上计算信息,要变动
value_type _up_(const value_type & ls, const value_type & rs) {// assert(0);return {min(ls[0], rs[0]), max(ls[1], rs[1]), ls[2] + rs[2]};
}/// 从上往下计算信息,要变动
void _dn_(int t, int s, int e, const lazy_type & delta) {// assert(0);if(delta.second == -1){ // 清零操作data[t] = value_zero();      lazy[t] = delta;  }else if(delta.second == 1){ // 增加一个权值, 必然是单点操作data[t][0] = min(data[t][0], delta.first);data[t][1] = max(data[t][1], delta.first);data[t][2] += delta.first;}    return;
}/// 初始化,清零,不用动
void init(int n) {data.assign((N = n) + 1 << 2, value_zero());lazy.assign(n + 1 << 2, lazy_zero());
}/// 这个函数不用动
void modify(int a, int b, const lazy_type & delta){_modify(1, 1, N, a, b, delta);
}/// 这个函数不用动
value_type query(int a, int b){return _query(1, 1, N, a, b);
}/// 几乎不用动
value_type _query(int t, int s, int e, int a, int b) {if(a <= s and e <= b) {return data[t];}_pushDown(t, s, e);int mid = (s + e) >> 1;value_type ans = value_zero(); // 如果求最值,这里不能用zeroif(a <= mid) ans = _up_(ans, _query(lson(t), s, mid, a, b));if(mid < b) ans = _up_(ans, _query(rson(t), mid + 1, e, a, b));return ans;
}/// 几乎不用动
void _modify(int t, int s, int e, int a, int b, const lazy_type & delta) {if(a <= s and e <= b) {_dn_(t, s, e, delta);return;}_pushDown(t, s, e);int mid = (s + e) >> 1;if(a <= mid) _modify(lson(t), s, mid, a, b, delta);if(mid < b) _modify(rson(t), mid + 1, e, a, b, delta);_pushUp(t);return;
}/// 这个函数不用动
void _pushUp(int t) {data[t] = _up_(data[lson(t)], data[rson(t)]);
}/// 这个函数几乎不用动
void _pushDown(int t, int s, int e) {if(lazy_zero() == lazy[t]) return;auto & lz = lazy[t];auto ls = lson(t), rs = rson(t);int mid = (s + e) >> 1;_dn_(ls, s, mid, lz);_dn_(rs, mid + 1, e, lz);lz = lazy_zero();
}/// 辅助函数,视延迟的类型而变动
static const lazy_type & lazy_zero() {static const lazy_type LAZY0 = {0, 0};return LAZY0; 
}/// 辅助函数,视线段树信息类型而变动
static const value_type & value_zero() {static const value_type VALUE0 = {INF, NINF, 0LL};return VALUE0;
}/// 这两个函数不用变动
static int lson(int x) {return x << 1;}
static int rson(int x) {return lson(x) | 1;}};struct dsu_on_tree_t{using vi = vector<int>;int N;
vector<vi> G; // 树, 1-indexstruct node_t{ // 树链剖分的结构int size; int hson; // 重儿子,这里是原树编号    int nid;  // 在树链剖分中的新编号int mdes; // 本子树全部在[nid, mdes]之间, 这是剖分编号
};
vector<node_t> Nodes;
vi New2Old;   // 剖分的编号为i,则原树节点编号为New2Old[i], 显然有Nodes[New2Old[i]].nid == i
int TimeStamp;
int Root;
vector<int> Depth;
SegTree St;
vector<long long> W;vector<array<llt, 3>> Ans;
vector<vector<array<int, 3>>> Questions;void init(int n){N = n;G.assign(N + 1, {});Nodes.assign(N + 1, {0, 0, 0, 0});New2Old.assign(N + 1, 0);TimeStamp = 0;Depth.assign(N + 1, 0);W.assign(N + 1, 0);St.init(N);
}void mkDiEdge(int a, int b){G[a].push_back(b);
}void mkBiEdge(int a, int b){mkDiEdge(a, b);mkDiEdge(b, a);
}void dfsHeavy(int u, int p){ // 递归重儿子Depth[u] = Depth[p] + 1;auto & n = Nodes[u];n.size = 1;New2Old[n.nid = ++TimeStamp] = u;for(auto v : G[u]){if(v == p) continue;dfsHeavy(v, u);n.size += Nodes[v].size;if(Nodes[n.hson].size < Nodes[v].size) n.hson = v;}n.mdes = TimeStamp;return;
}void dfs(int u, int p, bool keep){ // 递归const auto & n = Nodes[u];for(auto v : G[u]){if(v == p or v == n.hson) continue;dfs(v, u, false);}/// 最后递归重儿子if(n.hson) dfs(n.hson, u, true);/// 以下为统计u节点及其轻儿子if(n.hson){for(int i=n.nid;i<Nodes[n.hson].nid;++i){St.modify(Depth[New2Old[i]], Depth[New2Old[i]], {W[New2Old[i]], 1});}for(int i=Nodes[n.hson].mdes+1;i<=n.mdes;++i){St.modify(Depth[New2Old[i]], Depth[New2Old[i]], {W[New2Old[i]], 1});}}else{ // 只有一个节点St.modify(Depth[u], Depth[u], {W[u], 1});}/// 此时可以回答问题for(const auto & pp : Questions[u]){Ans[pp[2]] = St.query(Depth[u] + pp[0], Depth[u] + pp[1]);}/// 是否清空u子树对即时数据的影响if(not keep){/// 清空St.modify(1, N, {0LL, -1});}return;
}}Tree;int main(){
#ifndef ONLINE_JUDGEfreopen("z.txt", "r", stdin);
#endifios::sync_with_stdio(0); cin.tie(0); cout.tie(0);int n; cin >> n;Tree.init(n);   for(int i=1;i<=n;++i) cin >> Tree.W[i];for(int a,b,i=1;i<n;++i){cin >> a >> b;Tree.mkBiEdge(a, b);}int q; cin >> q;Tree.Questions.assign(n + 1, {});Tree.Ans.assign(q, {});for(int u,a,b,i=0;i<q;++i){cin >> u >> a >> b;Tree.Questions[u].push_back({a, b, i});}Tree.dfsHeavy(1, 0);Tree.dfs(1, 0, true);for(const auto & i : Tree.Ans) cout << i[0] << " " << i[1] << " " << i[2] << "\n";return 0;
}

这份代码与上一份的区别,在DSUonTree的部分,就是这里:

    if(not keep){/// 清空St.modify(1, N, {0LL, -1});}

不再逐个逐个节点的清空,而是一次性的清空整个线段树(注意,其实无需找出本子树深度的区间进行清空,直接清空整个即可)。当然,相应的线段树部分需要支持延迟操作。

NC235715

给定一个树,每次询问(u, k),即u子树内权值不大于k的节点数量。由于权值取值范围在 [ 1 , 1 0 6 ] [1, 10^6] [1,106],因此可以直接使用树状数组进行计数。每增加一个节点u,就将W[u]加1。需要查询时,只需要查询 [ 1 , k ] [1,k] [1,k]的区间和即可。清空还是一个个清空,将对应位置减一即可。

#include <bits/stdc++.h>
using namespace std;using llt = long long;
llt const INF = 0x1F2F3F4F5F6F7F8F;
llt const NINF = -INF;struct FenwickTree{ // 树状数组using value_type = long long int;
using vec_type = vector<value_type>;int n;
vec_type c;FenwickTree() = default;static int lowbit(int x){return x & -x;}void init(int nn){this->c.assign((this->n=nn) + 1, 0);}void modify(int pos, value_type delta){for(int i=pos;i<=this->n;i+=lowbit(i)) this->c[i] += delta;
}value_type query(int pos)const{value_type ans = 0;for(int i=pos;i;i-=lowbit(i)) ans += this->c[i];return ans;
}value_type query(int s, int e)const{return this->query(e) - this->query(s - 1);}};struct dsu_on_tree_t{using vi = vector<int>;int N;
vector<vi> G; // 树, 1-indexstruct node_t{ // 树链剖分的结构int size; int hson; // 重儿子,这里是原树编号    int nid;  // 在树链剖分中的新编号int mdes; // 本子树全部在[nid, mdes]之间, 这是剖分编号
};
vector<node_t> Nodes;
vi New2Old;   // 剖分的编号为i,则原树节点编号为New2Old[i], 显然有Nodes[New2Old[i]].nid == i
int TimeStamp;
int Root;FenwickTree Bt;
vector<long long> W;vector<int> Ans;
vector<vector<array<int, 2>>> Questions;void init(int n){N = n;G.assign(N + 1, {});Nodes.assign(N + 1, {0, 0, 0, 0});New2Old.assign(N + 1, 0);TimeStamp = 0;W.assign(N + 1, 0);Bt.init(1000001);
}void mkDiEdge(int a, int b){G[a].push_back(b);
}void mkBiEdge(int a, int b){mkDiEdge(a, b);mkDiEdge(b, a);
}void dfsHeavy(int u, int p){ // 递归重儿子// Depth[u] = Depth[p] + 1;auto & n = Nodes[u];n.size = 1;New2Old[n.nid = ++TimeStamp] = u;for(auto v : G[u]){if(v == p) continue;dfsHeavy(v, u);n.size += Nodes[v].size;if(Nodes[n.hson].size < Nodes[v].size) n.hson = v;}n.mdes = TimeStamp;return;
}void dfs(int u, int p, bool keep){ // 递归const auto & n = Nodes[u];for(auto v : G[u]){if(v == p or v == n.hson) continue;dfs(v, u, false);}/// 最后递归重儿子if(n.hson) dfs(n.hson, u, true);/// 以下为统计u节点及其轻儿子if(n.hson){for(int i=n.nid;i<Nodes[n.hson].nid;++i){Bt.modify(W[New2Old[i]], 1);}for(int i=Nodes[n.hson].mdes+1;i<=n.mdes;++i){Bt.modify(W[New2Old[i]], 1);}}else{ // 只有一个节点Bt.modify(W[u], 1);}/// 此时可以回答问题for(const auto & pp : Questions[u]){Ans[pp[1]] = Bt.query(pp[0]);}/// 是否清空u子树对即时数据的影响if(not keep){/// 清空for(int i=n.nid;i<=n.mdes;++i){Bt.modify(W[New2Old[i]], -1);}}return;
}}Tree;int main(){
#ifndef ONLINE_JUDGEfreopen("z.txt", "r", stdin);
#endifios::sync_with_stdio(0); cin.tie(0); cout.tie(0);int n; cin >> n;Tree.init(n);   for(int i=1;i<=n;++i) cin >> Tree.W[i];for(int a,b,i=1;i<n;++i){cin >> a >> b;Tree.mkBiEdge(a, b);}int q; cin >> q;Tree.Questions.assign(n + 1, {});Tree.Ans.assign(q, {});for(int u,a,i=0;i<q;++i){cin >> u >> a;Tree.Questions[u].push_back({a, i});}Tree.dfsHeavy(1, 0);Tree.dfs(1, 0, true);for(const auto & i : Tree.Ans) cout << i << "\n";return 0;
}

小结

总体而言,DSUonTree提供了一个离线查询的框架,其基础复杂度为 O ( N log ⁡ N ) O(N\log{N}) O(NlogN)。在此基础上需要解决数据的单点更新、按需查询以及清空三种操作。如果这三种操作都能在 log ⁡ N \log{N} logN及其以内时间完成,应该说DSUonTree就是可用的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/863209.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

pandas数据分析(1)

pandas&#xff0c;即Python数据分析库&#xff08;Python data analysis library&#xff09; DataFrame和Series DataFrame&#xff08;数据帧&#xff09;和Series&#xff08;序列&#xff09;是pandas的核心数据结构。DataFrame的主要组件包含索引、列、数据。DataFrame和…

“论模型驱动架构设计方法及其应用”,软考高级论文,系统架构设计师论文

论文真题 模型驱动架构设计是一种用于应用系统开发的软件设计方法&#xff0c;以模型构造、模型转换和精化为核心&#xff0c;提供了一套软件设计的指导规范。在模型驱动架构环境下&#xff0c;通过创建出机器可读和高度抽象的模型实现对不同问题域的描述&#xff0c;这些模型…

基于QT开发的气体成分检测数据记录软件

1、软件概述 气体成分检测数据记录软件用于实现多种气体分析仪及相关设备实时数据的获取、存储和传送。目前支持的设备主要有气体分析仪、多通道进样阀箱、冷阱处理系统和气体采样处理系统。   气体成分检测数据记录软件可以根据实际应用需要进行配置&#xff0c;以实现不同应…

算法 —— 双指针

目录 移动零 复写零 快乐数 盛最多水的容器 有效三角形的个数 查找总价格为目标值的两个商品 三数之和 四数之和 移动零 下图以样例1为例&#xff0c;看下图如何做到保证非零元素相对顺序前提下&#xff0c;移动零元素。 代码实现如下&#xff1a; class Solution {…

Python 学习之基础语法(一)

Python的语法基础主要包括以下几个方面&#xff0c;下面将逐一进行分点表示和归纳&#xff1a; 一、基本语法 1. 注释 a. 单行注释&#xff1a;使用#开头&#xff0c;例如# 这是一个单行注释。 b. 多行注释&#xff1a;使用三引号&#xff08;可以是三个单引号或三个双引号&…

MySQL高级-SQL优化- order by 优化(尽量使用覆盖索引、注意联合索引创建的规则、增大排序缓冲区大小)

文章目录 0、order by优化原则1、Using filesort2、Using index3、连接数据库4、查看索引5、删除索引6、按照年龄进行排序7、执行计划 order by age8、执行计划 order by age,phone9、创建联合索引 (age,phone)10、再次执行计划 order by age11、再次执行计划 order by age,pho…

013、MongoDB常用操作命令与高级特性深度解析

目录 MongoDB常用操作命令与高级特性深度解析 1. 数据库操作的深入探讨 1.1 数据库管理 1.1.1 数据库统计信息 1.1.2 数据库修复 1.1.3 数据库用户管理 1.2 数据库事务 2. 集合操作的高级特性 2.1 固定集合(Capped Collections) 2.2 集合验证(Schema Validation) 2.…

基于go1.19的站点模板爬虫

一、go1.19 go1.19是Go语言的一个版本,于2021年8月发布。它带来了许多新的功能和改进,包括但不限于以下方面: 并发性能改进:go1.19引入了新的调度器算法,称为“网状调度器(netlink scheduler)”,它可以更好地处理大量并发任务,在某些情况下提高了系统的并发能力。 垃…

计算机高并发浅析

一、 高并发简介 高并发应用场景涉及大量用户同时访问或操作系统&#xff0c;这对系统的性能、稳定性和扩展性提出了高要求。以下是一些常见的高并发应用场景及其复杂性简介&#xff1a; 1. 电商平台 场景&#xff1a;在大促销活动期间&#xff0c;如双十一、黑色星期五等&a…

标准版小程序订单中心path审核不通过处理教程

首先看自己小程序是不是已经审核通过并上线状态才在站内信里面提醒的&#xff1f; 如果没有提交过审核&#xff0c;请在提交的时候填写。path地址为&#xff1a;pages/goods/order_list/index 如果是已经上线的小程序&#xff0c;当时没要求填这个&#xff0c;但新的政策要求填…

聊天伴侣-豆包

前言 2024年5月14日凌晨&#xff0c;OpenAI发布最新多模态大模型 GPT-4o。GPT-4o的“o”代表“omni”&#xff0c;意为全能&#xff0c;与现有模型相比&#xff0c;它在视觉和音频理解方面尤其出色。GPT-4o可以在音频、视觉和文本中进行实时推理&#xff0c;接受文本、音频和图…

队列的相关知识

目录 创建 初始化 销毁 头插 尾删 取出头 取出尾 数字个数 判空 队列的性质与特征 性质&#xff1a;一种先进先出的线性表 特征&#xff1a;FIFO&#xff08;先进先出&#xff09; 实现&#xff1a;用数组和链表的都可以 例子&#xff1a;在生产者消费者模型用到了…

夏天到了,用这两款软件,悄悄惊艳所有人!

哈喽&#xff0c;各位小伙伴们好&#xff0c;我是给大家带来各类黑科技与前沿资讯的小武。 夏天来了&#xff0c;又到了“露肉”的季节&#xff0c;或许大家会为了身材烦恼&#xff0c;即便有运动意愿却苦于健身计划和时间上安排&#xff0c;也没有合适的免费软件。 别担心&a…

RpcRrovider分发rpc服务(OnMessage和Closure回调)

目录 1.完善rpcprovider.cc的OnConnection 2.完善rpcprovider.cc的OnMessage 3.完整rpcprovider.h 4.完整rpcprovider.cc 这篇文章主要完成&#xff0c;protobuf实现的数据序列化和反序列化。 1.完善rpcprovider.cc的OnConnection rpc的请求是短连接的&#xff0c;请求一次…

CICD持续集成(Jenkins+Git+Gogs)

1.Jenkins Jenkins 是一个开源的、用于构建和自动化软件开发流程的持续集成和交付工具。它提供了一个可扩展的平台&#xff0c;用于构建、测试和部署软件项目。通过使用 Jenkins&#xff0c;开发团队可以实现持续集成和交付&#xff0c;自动化构建和测试过程&#xff0c;提高软…

Web缓存代理和CDN 内容分发网络

目录 1.WEB缓存代理 1.1 WEB缓存代理作用 1.2 常见WEB缓存代理 1.3 Nginx 配置 缓存代理 2. CDN内容分发网络 1.WEB缓存代理 1.1 WEB缓存代理作用 存储一些之前给访问过的&#xff0c;且可能要被再次访问的静态网页资源对象&#xff0c;使客户端可以直接从缓存代理服务器…

【代码随想录训练营】【Day 60】【动态规划-13】| Leetcode 647, 516

【代码随想录训练营】【Day 60】【动态规划-13】| Leetcode 647, 516 需强化知识点 动态规划-回文系列 题目 647. 回文子串 注意sum&#xff08;dp&#xff09;不行j的遍历要倒序&#xff0c;并且 for j in range(i, n): class Solution:def countSubstrings(self, s: st…

C++Primer Plus 第十四章代码重用:14.4.7 成员模板

CPrimer Plus 第十四章代码重用 提示&#xff1a;这里可以添加系列文章的所有文章的目录&#xff0c;目录需要自己手动添加 例如&#xff1a;CPrimer Plus 第十四章代码重用&#xff1a;14.4.7 成员模板 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如…

KBL410-ASEMI智能AI专用整流桥KBL410

编辑&#xff1a;ll KBL410-ASEMI智能AI专用整流桥KBL410 型号&#xff1a;KBL410 品牌&#xff1a;ASEMI 封装&#xff1a;KBL-4 正向电流&#xff08;Id&#xff09;&#xff1a;4A 反向耐压&#xff08;VRRM&#xff09;&#xff1a;1000V 正向浪涌电流&#xff1a;2…

【计算机毕业设计】基于微信小程序的电子购物系统的设计与实现【源码+lw+部署文档】

包含论文源码的压缩包较大&#xff0c;请私信或者加我的绿色小软件获取 免责声明&#xff1a;资料部分来源于合法的互联网渠道收集和整理&#xff0c;部分自己学习积累成果&#xff0c;供大家学习参考与交流。收取的费用仅用于收集和整理资料耗费时间的酬劳。 本人尊重原创作者…