sword060's blog

By sword060, 7 months ago, In English

Introduction:

This blog covers how a DSU can save all of its previous versions after several union operations which there seems to be a lack of resources to discuss. Thanks to MinaRagy06 for helping me write this blog!

A basic DSU can be implemented in the following way:

DSU Code

Next, I will explain how to store more information to answer queries that require time travelling.

Main Idea:

Problem 1 (Easy) :

Let's start by solving a simple CSES Problem.

The problem can be reduced to binary searching on the first time nodes $$$a$$$ and $$$b$$$ are connected. Now let's learn how to check connectivity during any time using persistent DSU.

We maintain an extra array $$$\text{time_changed}$$$ where it stores the first time a node does not become a root after adding the edges in order. Now we can run $$$\text{get_root}$$$ with an extra parameter $$$\text{time}$$$ to find the root of that node during a certain time and check if nodes $$$a$$$ and $$$b$$$ have the same root at that specific time during binary search.

Time Complexity: $$$O(N + M \cdot {log} N + Q \cdot {log} M \cdot {log} N)$$$

Solution Code

Problem 2 (Medium) :

Consider the following problem, there is a Graph consisting of $$$N$$$ nodes and initially there are no edges you are given $$$Q$$$ queries which you have to solve online of the type:

  1. Add an Edge between node $$$A$$$ and node $$$B$$$

  2. Check Whether Node $$$A$$$ and Node $$$B$$$ are in the same connected component after the $$$X$$$-th Query

  3. Find the Number of Nodes in the connected component of Node $$$A$$$ after the $$$X$$$-th Query

Constraints:

$$$1 \le N,Q \le 2 \cdot 10^5$$$

$$$1 \le A,B \le N$$$

$$$1 \le X_i \le i$$$

In this problem, we also have to use the $$$\text{time_changed}$$$ array. In addition, we need to save the versions of each node such that it was a root after adding an edge in its component along with the size (or any new information we need to save in other problems).

We save two vectors for each node $$$\text{version}$$$ and $$$\text{size}$$$. whenever we add an edge, we push back the time to the new root along with the new total size.

Now whenever we want to get the size of component $$$A$$$ at time $$$T$$$, we find the root of $$$A$$$ at time $$$T$$$ and binary search on the largest index $$$pos$$$ such that $$$version[A][pos] \le T$$$ and return $$$size[A][pos]$$$ as the answer.

Time complexity: $$$O(N + Q \cdot {log} N)$$$

Solution Code

Problem 3 (Hard) :

$$$\textbf{Prerequisites: Persistent segment tree}$$$

Consider the following problem, initially you have $$$M = 1$$$ graphs of $$$N$$$ nodes with no edges, and you are given $$$Q$$$ queries which you have to solve online of the type:

  1. Copy the $$$k$$$-th graph and label the new graph $$$M + 1$$$ and set $$$M = M + 1$$$ then add a new edge connecting nodes $$$A$$$ and $$$B$$$ in this graph.

  2. Check whether node $$$A$$$ and node $$$B$$$ are in the same connected component in the $$$k$$$-th graph.

  3. Find the number of nodes in the connected component of node $$$A$$$ in the $$$k$$$-th graph.

Constraints:

$$$ 1 \le N,Q \le 2 \cdot 10^5 $$$

$$$ 1 \le K \le M $$$

$$$ 1 \le A,B \le N $$$

We can’t do the DSU in the same way mentioned in the previous problem since it allows us to update only the last version of the DSU but here we may have to update an older version. To solve this, we can use a persistent segment tree for every array instead, leaf $$$i$$$ would store the value of parent and size for index $$$i$$$ and any non-leaf won’t store anything except the left and right child. When updating $$$parent_i$$$ and $$$size_i$$$ for some index $$$i$$$ for a particular DSU version $$$k$$$, we can just refer to the node $$$i$$$ in the segment tree with root $$$k$$$ and change the leaf values we need to and label the new root $$$M + 1$$$ which will correspond to the DSU version $$$M + 1$$$.

Time complexity: $$$O( N + Q \cdot {log}^2 N)$$$

Solution Code

Extra Problems:

https://qoj.ac/problem/1217

https://codeforces.com/gym/104468/problem/B

https://oj.uz/problem/view/APIO20_swap

Thanks for reading!

Full text and comments »

  • Vote: I like it
  • +110
  • Vote: I do not like it

By sword060, history, 8 months ago, In English

Segment Tree is a powerful data structure in programming, that is why it can still be optimized way more. In this blog I will explain one optimization that can make a basic segment tree slightly faster and easier to write. (idea and the code by me)

This does not work on range update range query segment trees.

Introduction:

Let's consider a point update range query segment tree, while querying we visit many of useless Nodes along the way in order to answer the query moving from the root downwards.

As you can see, there are nodes (marked in red) that are not needed during the recursion, and we only need to visit the important nodes (marked in green).

This is only true when querying in a point update segment tree or updating in a point query segment tree.

Main Idea:

We can solve a query range $$$[l, r]$$$ by noticing we can make it a smaller range $$$[l + X , r]$$$, where $$$X$$$ is any power of two but we need it to be maximum (in order to reduce the time complexity) and these two conditions should be true:

  • $$$l + X - 1 \le r$$$. (We cannot go out of the range)
  • $$$[l, l + X - 1]$$$ is a valid node in the segment tree.
The first condition:
The second condition

At the end, we can solve it now because $$$X$$$ is $$$2$$$ power the minimum between $$$log_2(r-l+1)$$$ and $$$log_2( M \& -M )$$$ because it satisfies the first and second conditions and is the maximum value possible.

C++ Code:

We can preprocess $$$log_2(K)$$$ for each $$$1 \le K \le N$$$ in an array.

Note that this only works when $$$N$$$ (the number of leaves) is a power of 2.

At each step we calculate the size of the movement $$$X$$$ which is equal to $$$2^K$$$

The following codes calculate sum in the range $$$L$$$ to $$$R$$$, assuming the segment tree is built after possibly several update queries.

Recursive:

long long query(int l, int r){
	if(l > r)return 0;
	int node = N + l - 1;
	int K = min(logs[node & -node], logs[r - l + 1]);
	return (query(l + (1 << K), r) + seg[node >> K]);
}

Iterative:

long long query(int l, int r){
	long long ret = 0;
	while(l<=r){
		int node = N + l - 1;
		int K = min(logs[node & -node], logs[r - l + 1]);
		ret = (ret + seg[node >> K]);
		l += (1 << K);
	}
	return ret;
}

This can also be applied to range update point query segment trees:

void update(int l, int r, int val){
	while(l<=r){
		int node = N + l - 1;
                int K = min(logs[node & -node], logs[r - l + 1]);
		seg[node >> K] += val;
		lazy[node >> K] += val;
		l += (1 << K);
	}
}

Benchmark:

Test-Cases Generator
SD-Segment-Tree Code
Iterative-Segment-Tree Code
Recursive-Segment-Tree Code
Size of the array and the number of queries Time of SD-Segment-Tree /S Time of Recursive-Segment-Tree /S Time of Iterative-Segment-Tree /S
$$$N,Q = 2^{16}$$$ 00.2847 00.3163 00.2292
$$$N,Q = 2^{17}$$$ 00.4311 00.5335 00.4414
$$$N,Q = 2^{18}$$$ 00.8322 00.9534 00.9729
$$$N,Q = 2^{19}$$$ 01.9915 02.1086 01.6837
$$$N,Q = 2^{20}$$$ 03.6747 04.4253 03.7347
$$$N,Q = 2^{21}$$$ 08.0204 08.6896 07.7844
$$$N,Q = 2^{22}$$$ 20.9266 27.0589 24.3542
$$$N,Q = 2^{23}$$$ 50.0656 61.9385 49.8065

Conclusion:

This variation has the same time complexity as the normal segment tree $$$O(log(N))$$$ per query, but might need more memory if you preprocess Logs array.

The constant factor is smaller because of the unnecessary nodes we don't visit but in practice the time it takes is not significant than the normal segment tree for smaller array sizes.

This can only be useful for squeezing in time limits or for becoming an easier way to implement segment trees because it is shorter.

UPD: Added Benchmark

Full text and comments »

  • Vote: I like it
  • +123
  • Vote: I do not like it