题解:软件包管理器
一、题目分析
本题要求实现一个软件包管理器的依赖解决程序,给定软件包的依赖关系,处理安装和卸载软件包的操作,并快速返回操作实际改变安装状态的软件包数量。
二、解题思路
本题通过树链剖分结合线段树来处理软件包的依赖关系和操作。将软件包的依赖关系看作一棵树,利用树链剖分和线段树进行高效的区间更新和查询。
(一)数据结构与变量定义
const int N = 100010;
int n, m;
int h[N], e[N], ne[N], idx;
int id[N], cnt;
int dep[N], sz[N], top[N], fa[N], son[N];
struct Tree
{
int l, r, flag, sum;
}tr[N * 4];
N
:定义软件包数量的最大值。n
:存储软件包的总数。m
:存储操作(安装或卸载)的数量。h[N]
、e[N]
、ne[N]
、idx
:用于邻接表存储软件包依赖关系树的边信息,h
是表头数组,e
存储边的终点,ne
指向下一条边的索引,idx
是边的计数。id[N]
:数组,记录每个软件包在重链剖分后的新编号。cnt
:用于给软件包分配新编号的计数器。dep[N]
:数组,记录每个软件包所在节点的深度。sz[N]
:数组,记录以每个软件包所在节点为根的子树的节点数(即依赖该软件包及其子依赖的软件包数量)。top[N]
:数组,记录每个软件包所在重链的顶端节点。fa[N]
:数组,记录每个软件包的父软件包(即依赖的软件包)。son[N]
:数组,记录每个软件包所在节点的重儿子节点(子树节点数最多的子节点)。Tree
结构体:用于表示线段树的节点,包含区间左右端点l
、r
,懒标记flag
(表示该区间软件包的安装状态,-1表示未确定,0表示未安装,1表示已安装),区间内已安装软件包的数量sum
。
(二)辅助函数
- 添加边函数
add
:
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
向邻接表中添加一条从软件包a
到软件包b
的依赖边。
- 第一次深度优先搜索函数
dfs1
:
void dfs1(int u, int depth)
{
dep[u] = depth, sz[u] = 1;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
dfs1(j, depth + 1);
sz[u] += sz[j];
if (sz[son[u]] < sz[j]) son[u] = j;
}
}
计算每个软件包所在节点的深度、子树大小,并找出每个节点的重儿子。
- 第二次深度优先搜索函数
dfs2
:
void dfs2(int u, int t)
{
id[u] = ++ cnt, top[u] = t;
if (!son[u]) return;
dfs2(son[u], t);
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j == son[u]) continue;
dfs2(j, j);
}
}
对软件包依赖树进行重链剖分,给软件包分配新编号,确定每个软件包所在重链的顶端节点。
- 线段树向上更新函数
pushup
:
void pushup(int u)
{
tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}
根据子节点信息更新当前线段树节点的已安装软件包数量。
- 线段树向下更新函数
pushdown
:
void pushdown(int u)
{
auto &root = tr[u], &left = tr[u << 1], &right = tr[u << 1 | 1];
if (root.flag != -1)
{
left.sum = root.flag * (left.r - left.l + 1);
right.sum = root.flag * (right.r - right.l + 1);
left.flag = right.flag = root.flag;
root.flag = -1;
}
}
将当前线段树节点的懒标记下传给子节点,更新子节点的安装状态和已安装软件包数量。
- 线段树构建函数
build
:
void build(int u, int l, int r)
{
tr[u] = {l, r, -1, 0};
if (l == r) return;
int mid = l + r >> 1;
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
}
构建线段树,初始化每个节点的区间、懒标记和已安装软件包数量。
- 线段树区间更新函数
update
:
void update(int u, int l, int r, int k)
{
if (l <= tr[u].l && r >= tr[u].r)
{
tr[u].flag = k;
tr[u].sum = k * (tr[u].r - tr[u].l + 1);
return;
}
int mid = tr[u].l + tr[u].r >> 1;
pushdown(u);
if (l <= mid) update(u << 1, l, r, k);
if (r > mid) update(u << 1 | 1, l, r, k);
pushup(u);
}
在线段树中更新指定区间的软件包安装状态和已安装软件包数量。
- 路径安装状态更新函数
update_path
:
void update_path(int u, int v, int k)
{
while (top[u] != top[v])
{
if (dep[top[u]] < dep[top[v]]) swap(u, v);
update(1, id[top[u]], id[u], k);
u = fa[top[u]];
}
if (dep[u] < dep[v]) swap(u, v);
update(1, id[v], id[u], k);
}
更新软件包u
和软件包v
之间路径上所有软件包的安装状态。
- 子树安装状态更新函数
update_tree
:
void update_tree(int u, int k)
{
update(1, id[u], id[u] + sz[u] - 1, k);
}
更新以软件包u
为根的子树上所有软件包的安装状态。
(三)主函数main
int main()
{
scanf("%d", &n);
memset(h, -1, sizeof h);
for (int i = 2; i <= n; i ++ )
{
int p;
scanf("%d", &p);
p ++ ;
add(p, i);
fa[i] = p;
}
dfs1(1, 1);
dfs2(1, 1);
build(1, 1, n);
scanf("%d", &m);
char op[20];
int x;
while (m -- )
{
scanf("%s%d", op, &x);
x ++ ;
if (!strcmp(op, "install"))
{
int t = tr[1].sum;
update_path(1, x, 1);
printf("%d\n", tr[1].sum - t);
}
else
{
int t = tr[1].sum;
update_tree(x, 0);
printf("%d\n", t - tr[1].sum);
}
}
return 0;
}
- 读取软件包总数
n
,初始化邻接表,读取每个软件包的依赖关系,构建软件包依赖树的邻接表表示,并记录每个软件包的父软件包。 - 进行两次深度优先搜索,完成树链剖分,给软件包重新编号。
- 构建线段树。
- 读取操作数量
m
,依次处理每个操作:- 若操作是
install
(安装),先记录当前已安装软件包的数量t
,调用update_path
函数更新从根软件包到目标软件包路径上所有软件包的安装状态为已安装,然后输出安装操作实际改变安装状态的软件包数量(即更新后已安装软件包数量减去更新前的数量)。 - 若操作是
uninstall
(卸载),先记录当前已安装软件包的数量t
,调用update_tree
函数更新以目标软件包为根的子树上所有软件包的安装状态为未安装,然后输出卸载操作实际改变安装状态的软件包数量(即更新前已安装软件包数量减去更新后的数量)。
- 若操作是
四、时间复杂度和空间复杂度
(一)时间复杂度
- 初始化操作:包括读入数据、构建邻接表,时间复杂度为(O(n))。
- 树链剖分操作:两次深度优先搜索,时间复杂度为(O(n))。
- 线段树操作:构建线段树时间复杂度为(O(n)),每次安装或卸载操作(更新操作)时间复杂度为(O(\log n)),总共(m)次操作,所以这部分时间复杂度为(O(m\log n))。
- 总的时间复杂度为(O(n + n + n + m\log n)=O(n + m\log n))。
(二)空间复杂度
需要存储邻接表信息、软件包新编号id[N]
、深度数组dep[N]
、子树大小数组sz[N]
、重链顶端节点数组top[N]
、父软件包数组fa[N]
、重儿子节点数组son[N]
以及线段树节点tr[N * 4]
等,空间复杂度为(O(N + N + N + N + N + N + N + N\times4)=O(N)) 。