0% found this document useful (0 votes)
80 views41 pages

Segment Tree: Efficient Range Queries

The document provides an overview of Segment Trees and Tries, including their structure, operations, and applications. Segment Trees allow efficient querying and updating of array segments, while Tries are used for retrieving keys from strings based on common prefixes. Both data structures have advantages and disadvantages, with Segment Trees being useful for range queries and Tries for string manipulation tasks.

Uploaded by

vigneshs
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
80 views41 pages

Segment Tree: Efficient Range Queries

The document provides an overview of Segment Trees and Tries, including their structure, operations, and applications. Segment Trees allow efficient querying and updating of array segments, while Tries are used for retrieving keys from strings based on common prefixes. Both data structures have advantages and disadvantages, with Segment Trees being useful for range queries and Tries for string manipulation tasks.

Uploaded by

vigneshs
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd

61 CB 402- Design and Analysis of Algorithms

Unit 5
1. Segment Tree

Segment Tree is a data structure that allows efficient querying and updating of
intervals or segments of an array.

 It is particularly useful for problems involving range queries, such as finding


the sum, minimum, maximum, or any other operation over a specific range of
elements in an array.

 The tree is built recursively by dividing the array into segments until each
segment represents a single element.

 This structure enables fast query and update operations with a time complexity
of O(log n)

The following diagram shows a segment tree built for an array [1, 4, 5, 5, 9, 10, 10,
12, 19, 31, 41] of size 11. The example tree is built for range sum queries. Every node
stores sum of a range. The root nodes stores sum of the whole array and leaf nodes
store sums of single elements in the array.

Types of Operations:

The operations that the segment tree can perform must be binary and associative.
Some of the examples of operations are:
 Finding Range Sum Queries

 Searching index with given prefix sum

 Finding Range Maximum/Minimum

 Counting frequency of Range Maximum/Minimum

 Finding Range GCD/LCM

 Finding Range AND/OR/XOR

 Finding number of zeros in the given range or finding index of Kth zero

Structure of the Tree

The segment tree works on the principle of divide and conquer.

 At each level, we divide the array segments into two parts. If the given array
had [0, . . ., N-1] elements in it then the two parts of the array will be [0, . . .,
N/2-1] and [N/2, . . ., N-1].

 We will then recursively go on until the lower and upper bounds of the range
become equal.

 The structure of the segment tree looks like a binary tree.

The segment tree is generally represented using an array where the first value stores
the value for the total array range and the child of the node at the ith index are at (2*i
+ 1) and (2*i + 2).

Constructing the segment tree:

There are two important points to be noted while constructing the segment tree:

 Choosing what value to be stored in the nodes according to the problem


definition

 What should the merge operation do

If the problem definition states that we need to calculate the sum over ranges, then the
value at nodes should store the sum of values over the ranges.

 The child node values are merged back into the parent node to hold the value
for that particular range, [i.e., the range covered by all the nodes of its subtree].

 In the end, leaf nodes store information about a single element. All the leaf
nodes store the array based on which the segment tree is built.
Following are the steps for constructing a segment tree:

1. Start from the leaves of the tree

2. Recursively build the parents from the merge operation

The merge operation will take constant time if the operator takes constant time. SO
building the whole tree takes O(N) time.

Range Query

Let us understand this with the help of the following problem

Given two integers L and R return the sum of the segment [L, R]

The first step is constructing the segment tree with the addition operator and 0 as the
neutral element.

 If the range is one of the node's range values then simply return the answer.

 Otherwise, we will need to traverse the left and right children of the nodes and
recursively continue the process till we find a node that covers a range that
totally covers a part or whole of the range [L, R]

 While returning from each call, we need to merge the answers received from
each of its child.

As the height of the segment tree is logN the query time will be O(logN) per query.
Point Updates

Given an index, idx, update the value of the array at index idx with value V

The element's contribution is only in the path from its leaf to its parent. Thus
only logN elements will get affected due to the update.

For updating, traverse till the leaf that stores the value of index idx and update the
value. Then while tracing back in the path, modify the ranges accordingly.

The time complexity will be O(logN).

Example code:

Below is the implementation of construction, query, and point update for a segment
tree:
class SegmentTreeExample {

static int[] arr = {1, 3, 5, -2, 3};

static int n = [Link];

static int[] tree = new int[4 * n];

// Build Segment Tree

static void build(int node, int start, int end) {

if (start == end) {

tree[node] = arr[start];

return;

int mid = (start + end) / 2;

build(2 * node, start, mid);

build(2 * node + 1, mid + 1, end);

tree[node] = tree[2 * node] + tree[2 * node + 1];

// Update value at index

static void update(int node, int start, int end, int idx, int value) {

if (start == end) {

arr[idx] += value;

tree[node] += value;

return;

int mid = (start + end) / 2;

if (idx <= mid)

update(2 * node, start, mid, idx, value);

else
update(2 * node + 1, mid + 1, end, idx, value);

tree[node] = tree[2 * node] + tree[2 * node + 1];

// Range Sum Query

static int query(int node, int start, int end, int l, int r) {

if (r < start || l > end)

return 0;

if (l <= start && end <= r)

return tree[node];

int mid = (start + end) / 2;

return query(2 * node, start, mid, l, r)

+ query(2 * node + 1, mid + 1, end, l, r);

public static void main(String[] args) {

build(1, 0, n - 1);

[Link]("Sum from index 0 to 3: "

+ query(1, 0, n - 1, 0, 3));

update(1, 0, n - 1, 1, 100);

[Link]("After updating index 1 by +100");

[Link]("Sum from index 1 to 3: "

+ query(1, 0, n - 1, 1, 3));

Output

Sum from index 0 to 3: 7


After updating index 1 by +100

Sum from index 1 to 3: 106

Time complexity: O(N)

 The building operation takes O(N) time

 The query operation takes O(logN) time

 Each update is performed in O(logN) time

Auxiliary Space: O(n)

Updating an interval (Lazy propagation):

Lazy Propagation: A speedup technique for range updates

 We can delay some updates (avoid recursive calls in update) and do such
updates only when necessary when there are several updates and updates are
being performed on a range.

 A node in a segment tree stores or displays the results of a query for a variety
of indexes.
 Additionally, all of the node's descendants must also be updated if the update
operation's range includes this node.

o Take the node with the value 27 in the picture above as an example. This
node contains the sum of values at the indexes 3 to 5. This node and all
of its descendants must be updated if our update query covers the range
of 2 to 5.

o By storing this update information in distinct nodes referred to as lazy


nodes or values, we use lazy propagation to update only the node with
value 27 and delay updates to its descendants.

 We make an array called lazy[] to stand in for the lazy node. The size
of lazy[] is the same as the array used to represent the segment tree in the code
following, which is tree[].

 The goal is to set all of the lazy[elements] to 0.

o There are no pending changes on the segment tree node i if lazy[i] has a
value of 0.

o A non-zero value for lazy[i] indicates that before doing any queries on
node i in the segment tree, this sum needs to be added to the node.

Time_Complexity: O(N)
Auxiliary Space: O(MAX)

Applications:

 Interval scheduling: Segment trees can be used to efficiently schedule non-


overlapping intervals, such as scheduling appointments or allocating resources.

 Range-based statistics: Segment trees can be used to compute range-based


statistics such as variance, standard deviation, and percentiles.

 Image processing: Segment trees are used in image processing algorithms to


divide an image into segments based on color, texture, or other attributes.

Advantages:

 Efficient querying: Segment trees can be used to efficiently answer queries


about the minimum, maximum, sum, or other aggregate value of a range of
elements in an array.

 Efficient updates: Segment trees can be used to efficiently update a range of


elements in an array, such as incrementing or decrementing a range of values.
 Flexibility: Segment trees can be used to solve a wide variety of problems
involving range queries and updates.

Disadvantages:

 Complexity: Segment trees can be complex to implement and maintain,


especially for large arrays or high-dimensional data.

 Time complexity: The time complexity of segment tree operations like build,
update, and the query is O(log N) , which is higher than some other data
structures like the Fenwick tree.

 Space complexity: The space complexity of a segment tree is O(4N) which is


relatively high.

2. Tries

A trie is a type of a multi-way search tree, which is fundamentally used to retrieve


specific keys from a string or a set of strings. It stores the data in an ordered efficient
way since it uses pointers to every letter within the alphabet.

The trie data structure works based on the common prefixes of strings. The root node
can have any number of nodes considering the amount of strings present in the set.
The root of a trie does not contain any value except the pointers to its child nodes.

There are three types of trie data structures −

 Standard Tries

 Compressed Tries

 Suffix Tries

The real-world applications of trie include − autocorrect, text prediction, sentiment


analysis and data sciences.
Basic Operations in Tries

The trie data structures also perform the same operations that tree data structures
perform. They are −

 Insertion

 Deletion

 Search

Insertion operation

The insertion operation in a trie is a simple approach. The root in a trie does not hold
any value and the insertion starts from the immediate child nodes of the root, which
act like a key to their child nodes. However, we observe that each node in a trie
represents a singlecharacter in the input string. Hence the characters are added into the
tries one by one while the links in the trie act as pointers to the next level nodes.

Example

import [Link];

import [Link];

class TrieNode {

Map<Character, TrieNode> children;

boolean isEndOfWord;

TrieNode() {

children = new HashMap<>();

isEndOfWord = false;

class Trie {

private TrieNode root;

Trie() {

root = new TrieNode();


}

void insert(String word) {

TrieNode curr = root;

for (char ch : [Link]()) {

[Link](ch, new TrieNode());

curr = [Link](ch);

[Link] = true;

TrieNode getRoot() {

return root;

public class Main {

public static void printWords(TrieNode node, String prefix) {

if ([Link]) {

[Link](prefix);

for ([Link]<Character, TrieNode> entry : [Link]()) {

printWords([Link](), prefix + [Link]());

public static void main(String[] args) {

Trie car = new Trie();


// Inserting the elements

[Link]("Lamborghini");

[Link]("Mercedes-Benz");

[Link]("Land Rover");

[Link]("Maruti Suzuki");

// Print the inserted objects

[Link]("Tries elements are: \n");

printWords([Link](), ""); // Access root using the public method

Output

Tries elements are:

Lamborghini

Land Rover

Maruti Suzuki

Mercedes-Benz

Deletion operation

The deletion operation in a trie is performed using the bottom-up approach. The
element is searched for in a trie and deleted, if found. However, there are some special
scenarios that need to be kept in mind while performing the deletion operation.

Case 1 − The key is unique − in this case, the entire key path is deleted from the node.
(Unique key suggests that there is no other path that branches out from one path).

Case 2 − The key is not unique − the leaf nodes are updated. For example, if the key
to be deleted is see but it is a prefix of another key seethe; we delete the see and
change the Boolean values of t, h and e as false.

Case 3 − The key to be deleted already has a prefix − the values until the prefix are
deleted and the prefix remains in the tree. For example, if the key to be deleted
is heart but there is another key present he; so we delete a, r, and t until only he
remains.
Example

//Java code for Deletion operator of tries algotrithm

import [Link];

import [Link];

class TrieNode {

Map<Character, TrieNode> children;

boolean isEndOfWord;

TrieNode() {

children = new HashMap<>();

isEndOfWord = false;

class Trie {

private TrieNode root;

Trie() {

root = new TrieNode();

void insert(String word) {

TrieNode curr = root;

for (char ch : [Link]()) {

[Link](ch, new TrieNode());

curr = [Link](ch);

[Link] = true;

}
TrieNode getRoot() {

return root;

boolean delete(String word) {

return deleteHelper(root, word, 0);

private boolean deleteHelper(TrieNode curr, String word, int index) {

if (index == [Link]()) {

if (![Link]) {

return false; // Word does not exist in the Trie

[Link] = false; // Mark as deleted

return [Link](); // Return true if no more children

char ch = [Link](index);

if (![Link](ch)) {

return false; // Word does not exist in the Trie

TrieNode child = [Link](ch);

boolean shouldDeleteChild = deleteHelper(child, word, index + 1);

if (shouldDeleteChild) {

[Link](ch); // Remove the child node if necessary

return [Link](); // Return true if no more children

return false;
}

public class Main {

public static void printWords(TrieNode node, String prefix) {

if ([Link]) {

[Link](prefix);

for ([Link]<Character, TrieNode> entry : [Link]()) {

printWords([Link](), prefix + [Link]());

public static void main(String[] args) {

Trie car = new Trie();

//Inserting the elements

[Link]("Lamborghini");

[Link]("Mercedes-Benz");

[Link]("Land Rover");

[Link]("Maruti Suzuki");

//Before Deletion

[Link]("Tries elements before deletion: ");

printWords([Link](), "");

String s1 = "Lamborghini";

String s2 = "Land Rover";

[Link]("Element to be deleted are: \n" + s1 + " and " + s2);

[Link](s1);
[Link](s2);

[Link]("\nTries elements after deletion: ");

printWords([Link](), "");

Output

Tries elements before deletion:

lamborghini

landrover

marutiouzuki

mercezenz

Elements to be deleted are: lamborghini and landrover

Tries elements before deletion:

marutiouzuki

mercezenz

Search operation

Searching in a trie is a rather straightforward approach. We can only move down the
levels of trie based on the key node (the nodes where insertion operation starts at).
Searching is done until the end of the path is reached. If the element is found, search is
successful; otherwise, search is prompted unsuccessful.

Example

//Java program for tries Algorithm

import [Link];

import [Link];

class TrieNode {

Map<Character, TrieNode> children;

boolean isEndOfWord;
TrieNode() {

children = new HashMap<>();

isEndOfWord = false;

class Trie {

private TrieNode root;

Trie() {

root = new TrieNode();

void insert(String word) {

TrieNode curr = root;

for (char ch : [Link]()) {

[Link](ch, new TrieNode());

curr = [Link](ch);

[Link] = true;

TrieNode getRoot() {

return root;

boolean search(String word) {

TrieNode curr = root;

for (char ch : [Link]()) {

if (![Link](ch)) {

return false;
}

curr = [Link](ch);

return [Link];

boolean startsWith(String prefix) {

TrieNode curr = root;

for (char ch : [Link]()) {

if (![Link](ch)) {

return false;

curr = [Link](ch);

return true;

public class Main {

public static void printWords(TrieNode node, String prefix) {

if ([Link]) {

[Link](prefix);

for ([Link]<Character, TrieNode> entry : [Link]()) {

printWords([Link](), prefix + [Link]());

public static void main(String[] args) {


Trie car = new Trie();

//Inserting the elements

[Link]("Lamborghini");

[Link]("Mercedes-Benz");

[Link]("Land Rover");

[Link]("Maruti Suzuki");

[Link]("Tries elements are: \n");

printWords([Link](), " ");

//searching the elements

[Link]("Searching Cars");

//Printing the searched elements

[Link]("Found? " + [Link]("Lamborghini")); // Output: true

[Link]("Found? " + [Link]("Mercedes-Benz")); // Output: true

[Link]("Found? " + [Link]("Honda")); // Output: false

[Link]("Found? " + [Link]("Land Rover")); // Output: true

[Link]("Found? " + [Link]("BMW")); // Output: false

//searching the elements name start with?

[Link]("Cars name starts with");

//Printing the elements

[Link]("Does car name starts with 'Lambo'? " +


[Link]("Lambo")); // Output: true

[Link]("Does car name starts with 'Hon'? " + [Link]("Hon"));


// Output: false

[Link]("Does car name starts with 'Hy'? " + [Link]("Hy"));


// Output: false

[Link]("Does car name starts with 'Mer'? " +[Link]("Mar"));


// Output: true
[Link]("Does car name starts with 'Land'? " + [Link]("Land"));
// Output: true

Output

Tries elements are:

Lamborghini

Land Rover

Maruti Suzuki

Mercedes-Benz

Searching Cars

Found? true

Found? true

Found? false

Found? true

Found? false

Cars name starts with

Does car name starts with 'Lambo'? true

Does car name starts with 'Hon'? false

Does car name starts with 'Hy'? false

Does car name starts with 'Mer'? true

Does car name starts with 'Land'? true

3. Game Theory

Game Theory is a topic in competitive programming that involves a certain type of


problem, where there are some players who play a game based on given rules and the
task is often to find the winner or the winning moves. Game Theory is often asked in
short contests with a mixture of other topics like range querying or greedy or dynamic
programming.

Game Theory for Competitive Programming:

 Here we will focus on two-player games that do not contain random elements.

 Our goal is to find a strategy we can follow to win the game no matter what the
opponent does if such a strategy exists.

 Game theory or combinatorics game theory in which we have perfect


information (that is no randomization like a coin toss) such as game rules,
player's turn, minimum and maximum involved in the problem statements, and
some conditions and constraints.

 There will be three possible cases/ state win, loss or tie.

 A terminal condition is well-defined/ specified clearly.


E.g. player who picks the last coin will win the game, or a player who picks the
second last time coin will win the game or something like that.

 It is assumed that the game will end at some point after a fixed number of
moves. Unlike chess, where you can have an unlimited number of moves
possible especially when you are left with the only king, but if you add an extra
constraint that says “game should be ended within ‘n’ numbers of moves”, that
will be a terminal condition. This is the kind of assumption a game theory is
looking for.

 It turns out that there is a general strategy for such games, and we can analyze
the games using the nim theory.

 Initially, we will analyze simple games where players remove sticks from
heaps, and after this, we will generalize the strategy used in those games to
other games.

3.1 Game states:

Let us consider a game where there is initially a heap of n-sticks. Players A and B
move alternately, and player A begins. On each move, the player has to remove 1, 2,
or 3 sticks from the heap, and the player who removes the last stick wins the game.

For example, if n = 10, the game may proceed as follows:

 A → removes 2 sticks (8 sticks left).

 B → removes 3 sticks (5 sticks left).


 A → removes 1 stick (4 sticks left)

 B → removes 2 sticks (2 sticks left).

 A → removes 2 sticks and wins

This game consists of states 0, 1, 2,..., n, where the number of the state corresponds to
the number of sticks left.

A few examples of Game states are:

 Tic Tac Toe = Tic Tac Toe is a classic two-player game where the players take
turns placing either X or O in a 3x3 grid until one player gets three in a row
horizontally, vertically, or diagonally, or all spaces on the board are filled.

 Rock-Paper-Scissors = Rock-Paper-Scissors is a simple two-player game


where each player simultaneously chooses one of three options (rock, paper,
scissors). The winner is determined by a set of rules, rock beats scissors,
scissors beat paper, and paper beats rock.

3.2 Winning and Losing states:

A winning state is a state where the player will win the game if they play optimally,
and a Losing state is a state where the player will lose the game if the opponent plays
optimally. It turns out that we can classify all states of a game so that each state is
either a winning state or a losing state

Let's consider the above game:

In the above game, state 0 is clearly a losing state because the player cannot make any
moves.

 States 1, 2, and 3 are winning states because we can remove 1, 2, or 3 sticks


and win the game.

 State 4, in turn, is a losing state, because any move leads to a state that is a
winning state for the opponent.

More generally, if there is a move that leads from the current state to a losing state, the
current state is a winning state, and otherwise, the current state is a losing state.
Using this observation, we can classify all states of a game starting with losing states
where there are no possible [Link] states 0...15 of the above game can be
classified as follows (W denotes a winning state and L denotes a losing state):
Stat 1 1 1 1 1 1
0 1 2 3 4 5 6 7 8 9
es 0 1 2 3 4 5

Res W
L W W L W W W L W W W L W W W
ult

It is easy to analyze this game: A state k is a losing state if k is divisible by 4, and


otherwise, it is a winning state. An optimal way to play the game is to always choose a
move after which the number of sticks in the heap is divisible by 4. Finally, there are
no sticks left and the opponent has lost. Of course, this strategy requires that the
number of sticks is not divisible by 4 when it is our move. If it is, there is nothing we
can do, and the opponent will win the game if they play optimally.

Example:-

Basketball = In basketball, a winning state is when a team scores more points than
their opponent at the end of the game, while a losing state is when a team scores
fewer points than their opponent.

Chess = In chess, a winning state is when a player checkmates their opponent's king,
while a losing state is when a player's king is checkmated.

State graph:

Let us now consider another stick game, where in each state k, it is allowed to remove
any number x of sticks such that x is smaller than k and divides k.

For example, in state 8 we may remove 1, 2 or 4 sticks, but in state 7 the only
allowed move is to remove 1 stick. The following picture shows the states 1...9 of the
game as a state graph, whose nodes are the states and edges are the moves between
them:
The states 1...9 of the game as a state graph, whose nodes are the states and edges are
the moves between them

The final state in this game is always state 1, which is a losing state because there are
no valid moves. The classification of states 1...9 is as follows:

1 2 3 4 5 6 7 8 9

L W L W L W L W L

Surprisingly, in this game, all even-numbered states are winning states, and all odd-
numbered states are losing states

3.3 Nim game:

The nim game is a simple game that has an important role in game theory because
many other games can be played using the same strategy.
First, we focus on nim, and then we generalize the strategy to other games.
There are n heaps in nim, and each heap contains some number of sticks.
The players move alternately, and on each turn, the player chooses a heap that still
contains sticks and removes any number of sticks from it.
The winner is the player who removes the last stick.

The states in nim are of the form [x1, x2,..., xn], where xk denotes the number of sticks
in heap k.

For example, A[] = [10,12,5]


It is a game where there are three heaps with 10, 12 and 5 sticks.
The state [0,0,...,0] is a losing state, because it is not possible to remove any sticks,
and this is always the final state.

Analysis:

⊕ x2 ⊕··· ⊕ xn, where ⊕ is the xor operation.


It turns out that we can easily classify any nim state by calculating the nim sum s = x1

The states whose nim sum is 0 are losing states, and all other states are winning states.
For example, the nim sum of [10,12,5] is 10⊕12⊕5 = 3, so the state is a winning state.

Losing states:

The final state [0,0,...,0] is a losing state, and its nim sum is 0, as expected.
In other losing states, any move leads to a winning state, because when a single value
xk changes, the nim sum also changes, so the nim sum is different from 0 after the
move.

Winning states:

We can move to a losing state if there is any heap k for which xk ⊕ s < xk.
In this case, we can remove sticks from heap k so that it will contain xk ⊕ s sticks,
which will lead to a losing state.

There is always such a heap, where xk has a one bit at the position of the leftmost one
bit of s.

As an example:

consider the state [10,12,5].

This state is a winning state because its nim sum is 3. Thus, there has to be a move
that leads to a losing state. Next, we will find out such a move. The nim sum of the
state is as follows:

10 1010
12 1100
5 0101

3 0011
In this scenario, the heap with 10 sticks is the only heap that has a one bit at the
position of the leftmost one bit of the nim sum:

10 1010
12 1100
5 0101

3 0011

The new size of the heap has to be 10⊕ 3 = 9, so we will remove just one stick. After
this, the state will be [9,12,5], which is a losing state:

9 1001
12 1100
5 0101

0 0000

3.4 Misère game:

In a misère game, the goal of the game is the opposite, so the player who removes the
last stick loses the game.

It turns out that the misère nim game can be optimally played almost like the standard
nim game.

The idea is to first play the misère game like the standard game, but change the
strategy at the end of the game.

The new strategy will be introduced in a situation where each heap would contain at
most one stick after the next move.

In the standard game, we should choose a move after which there is an even number
of heaps with one stick.

However, in the misère game, we choose a move so that there is an odd number of
heaps with one stick.
This strategy works because a state where the strategy changes always appear in the
game, and this state is a winning state because it contains exactly one heap that has
more than one stick so the nim sum is not 0.

3.5 Sprague–Grundy theorem:

The Sprague–Grundy theorem generalizes the strategy used in nim to all games that
fulfil the following requirements:

 Two players move alternately.

 The game consists of states, and the possible moves in a state do not depend
on whose turn it is.

 The game ends when a player cannot make a move.

 The game surely ends sooner or later.

 The players have complete information about the states and allowed moves,
and there is no randomness in the game.

The idea is to calculate for each game state a Grundy number that corresponds to the
number of sticks in a nim heap. When we know the Grundy numbers of all states, we
can play the game like the nim game.

3.6 Grundy numbers:

The Grundy number of a game state is mex({g1, g2,..., gn}).

where g1, g2,..., gn are the Grundy numbers of the states to which we can move, and
the mex function gives the smallest non-negative number that is not in the set.

For example:

mex({0,1,3}) = 2. If there are no possible moves in a state, its Grundy number is 0,


because mex(Ø) = 0.\

For example in the state graph:

The Grundy numbers are as follows:


The Grundy number of a losing state is 0, and the Grundy number of a winning state is
a positive number.

The Grundy number of a state corresponds to the number of sticks in a nim heap. If
the Grundy number is 0, we can only move to states whose Grundy numbers are
positive, and if the Grundy number is x > 0, we can move to states whose Grundy
numbers include all numbers 0,1,..., x−1.

As an example:

Example: consider a game where the players move a figure in a maze.

 Each square in the maze is either a floor or a wall.

 On each turn, the player has to move the figure some number of steps left or
up.

 The winner of the game is the player who makes the last move.

The following picture shows a possible initial state of the game, where @ denotes the
figure and # denotes a square where it can move.

The states of the game are all floor squares of the maze. In the above maze, the
Grundy numbers are as follows:
Thus, each state of the maze game corresponds to a heap in the nim game. For
example, the Grundy number for the lower-right square is 2, so it is a winning state.
We can reach a losing state and win the game by moving either four steps left or two
steps up.

Note that unlike in the original nim game, it may be possible to move to a state whose
Grundy number is larger than the Grundy number of the current state.
However, the opponent can always choose a move that cancels such a move, so it is
not possible to escape from a losing state.

3.7 Subgames:

Next, we will assume that our game consists of subgames, and on each turn, the player
first chooses a subgame and then move into the subgame. The game ends when it is
not possible to make any move in any [Link] this case, the Grundy number of a
game is the nim sum of the Grundy numbers of the subgames.
The game can be played like a nim game by calculating all Grundy numbers for
subgames and then their nim sum.

As an example, consider a game that consists of three mazes. In this game, on each
turn, the player chooses one of the mazes and then moves the figure in the maze.
Assume that the initial state of the game is as follows:

the player chooses one of the mazes and then moves the figure in the maze. Assume
that the initial state of the game is as follows
The Grundy numbers for the mazes are as follows

In the initial state, the nim sum of the Grundy numbers is 2⊕3⊕3 = 2, so the first
player can win the game. One optimal move is to move two steps up in the first maze,
which produces the nim sum 0⊕3⊕3 = 0.

3.8 Grundy’s game:

Sometimes a move in a game divides the game into subgames that are independent of
each other. In this case, the Grundy number of the game is mex({g1, g2,..., gn}),

where n is the number of possible moves and gk = ak,1 ⊕ ak,2 ⊕...⊕ ak,m,

where move k generates subgames with Grundy numbers ak,1,ak,2,...,ak,m.

An example of such a game is Grundy’s game. Initially, there is a single heap that
contains n sticks.

On each turn, the player chooses a heap and divides it into two nonempty heaps such
that the heaps are of different size. The player who makes the last move wins the
game.
Let f (n) be the Grundy number of a heap that contains n sticks. The Grundy number
can be calculated by going through all ways to divide the heap into two heaps.

(1)⊕ f (7), f (2)⊕ f (6), f (3)⊕ f (5)}).


For example, when n = 8, the possibilities are 1+7, 2+6 and 3+5, so f(8) = mex({f

In this game, the value of f (n) is based on the values of f (1),..., f (n−1). The base
cases are f (1) = f (2) = 0, because it is not possible to divide the heaps of 1 and 2
sticks. The first Grundy numbers are:

f (1) = 0
f (2) = 0
f (3) = 1
f (4) = 0
f (5) = 2
f (6) = 1
f (7) = 0
f (8) = 2

The Grundy number for n = 8 is 2, so it is possible to win the game. The winning
move is to create heaps 1+7 because f (1)⊕ f (7) = 0.

4. Computability of Algorithms

In computer science, problems are divided into classes known as Complexity Classes.
In complexity theory, a Complexity Class is a set of problems with related complexity.
With the help of complexity theory, we try to cover the following.

 Problems that cannot be solved by computers.

 Problems that can be efficiently solved (solved in Polynomial time) by


computers.

 Problems for which no efficient solution (only exponential time algorithms)


exist.

The common resources required by a solution are are time and space, meaning how
much time the algorithm takes to solve a problem and the corresponding memory
usage.

 The time complexity of an algorithm is used to describe the number of steps


required to solve a problem, but it can also be used to describe how long it
takes to verify the answer.

 The space complexity of an algorithm describes how much memory is required


for the algorithm to operate.

 An algorithm having time complexity of the form O(nk) for input n and
constant k is called polynomial time solution. These solutions scale well. On
the other hand, time complexity of the form O(kn) is exponential time.

Complexity classes are useful in organizing similar types of problems.


Types of Complexity Classes

This article discusses the following complexity classes:

4.1 P Class

The P in the P class stands for Polynomial Time. It is the collection of decision
problems(problems with a "yes" or "no" answer) that can be solved by a deterministic
machine (our computers) in polynomial time.

Features:

 The solution to P problems is easy to find.

 P is often a class of computational problems that are solvable and tractable.


Tractable means that the problems can be solved in theory as well as in
practice. But the problems that can be solved in theory but not in practice are
known as intractable.

Most of the coding problems that we solve fall in this category like the below.

1. Calculating the greatest common divisor.

2. Finding a maximum matching.

3. Merge Sort

4.2 NP Class

The NP in NP class stands for Non-deterministic Polynomial Time. It is the


collection of decision problems that can be solved by a non-deterministic machine
(note that our computers are deterministic) in polynomial time.
Features:

 The solutions of the NP class might be hard to find since they are being solved
by a non-deterministic machine but the solutions are easy to verify.

 Problems of NP can be verified by a deterministic machine in polynomial time.

Example:

Let us consider an example to better understand the NP class. Suppose there is a


company having a total of 1000 employees having unique employee IDs. Assume that
there are 200 rooms available for them. A selection of 200 employees must be paired
together, but the CEO of the company has the data of some employees who can't work
in the same room due to personal reasons.
This is an example of an NP problem. Since it is easy to check if the given choice
of 200 employees proposed by a coworker is satisfactory or not i.e. no pair taken from
the coworker list appears on the list given by the CEO. But generating such a list from
scratch seems to be so hard as to be completely impractical.

It indicates that if someone can provide us with the solution to the problem, we can
find the correct and incorrect pair in polynomial time. Thus for the NP class problem,
the answer is possible, which can be calculated in polynomial time.

This class contains many problems that one would like to be able to solve effectively:

1. Boolean Satisfiability Problem (SAT).

2. Hamiltonian Path Problem.

3. Graph coloring.

Co-NP Class

Co-NP stands for the complement of NP Class. It means if the answer to a problem in
Co-NP is No, then there is proof that can be checked in polynomial time.

Features:

 If a problem X is in NP, then its complement X' is also in CoNP.

 For an NP and CoNP problem, there is no need to verify all the answers at once
in polynomial time, there is a need to verify only one particular answer "yes" or
"no" in polynomial time for a problem to be in NP or CoNP.

Some example problems for CoNP are:

1. To check prime number.


2. Integer Factorization.

4.3 NP-hard class

An NP-hard problem is at least as hard as the hardest problem in NP and it is a class of


problems such that every problem in NP reduces to NP-hard.

Features:

 All NP-hard problems are not in NP.

 It takes a long time to check them. This means if a solution for an NP-hard
problem is given then it takes a long time to check whether it is right or not.

 A problem A is in NP-hard if, for every problem L in NP, there exists a


polynomial-time reduction from L to A.

Some of the examples of problems in Np-hard are:

1. Halting problem.

2. Qualified Boolean formulas.

3. No Hamiltonian cycle.

4.4 NP-complete class

A problem is NP-complete if it is both NP and NP-hard. NP-complete problems are


the hard problems in NP.

Features:

 NP-complete problems are special as any problem in NP class can be


transformed or reduced into NP-complete problems in polynomial time.

 If one could solve an NP-complete problem in polynomial time, then one could
also solve any NP problem in polynomial time.

Some example problems include:

1. Hamiltonian Cycle.

2. Satisfiability.

3. Vertex cover.
Complexity
Characteristic feature
Class

P Easily solvable in polynomial time.

NP Yes, answers can be checked in polynomial time.

Co-NP No, answers can be checked in polynomial time.

All NP-hard problems are not in NP and it takes a long time to


NP-hard
check them.

NP-complete A problem that is NP and NP-hard is NP-complete.

5_NP Complete Problems

5.1 Hamiltonian Cycle:

A cycle in an undirected graph G=(V, E) traverses every vertex exactly once.

Problem Statement: Given a graph G=(V, E), the problem is to determine if graph G
contains a Hamiltonian cycle consisting of all the vertices belonging to V.

Explanation: An instance of the problem is an input specified to the problem. An


instance of the Independent Set problem is a graph G=(V, E), and the problem is to
check whether the graph can have a Hamiltonian Cycle in G. Since an NP-Complete
problem, by definition, is a problem which is both in NP and NP-hard, the proof for
the statement that a problem is NP-Complete consists of two parts:

1. The problem itself is in NP class.

2. All other problems in NP class can be polynomial-time reducible to that. (B is


polynomial-time reducible to C is denoted as [Tex]B$\leqslant_P$C [/Tex])

If the 2nd condition is only satisfied then the problem is called NP-Hard. But it is not
possible to reduce every NP problem into another NP problem to show its NP-
Completeness all the time. That is why if we want to show a problem is NP-Complete,
we just show that the problem is in NP and if any NP-Complete problem is reducible
to that, then we are done, i.e. if B is NP-Complete and [Tex]B$\
leqslant_P$C [/Tex]for C in NP, then C is NP-Complete.

1. Hamiltonian Cycle is in NP If any problem is in NP, then, given a ‘certificate’,


which is a solution to the problem and an instance of the problem (a graph G and a
positive integer k, in this case), we will be able to verify (check whether the solution
given is correct or not) the certificate in polynomial time. The certificate is a sequence
of vertices forming Hamiltonian Cycle in the graph. We can validate this solution by
verifying that all the vertices belong to the graph and each pair of vertices belonging
to the solution are adjacent. This can be done in polynomial time, that is O(V
+E) using the following strategy for graph G(V, E):

flag=true

For every pair {u, v} in the subset V’:

Check that these two have an edge between them

If there is no edge, set flag to false and break

If flag is true:

Solution is correct

Else:

Solution is incorrect

Hamiltonian Cycle is NP Hard In order to prove the Hamiltonian Cycle is NP-Hard,


we will have to reduce a known NP-Hard problem to this problem. We will carry out a
reduction from the Hamiltonian Path problem to the Hamiltonian Cycle problem.
Every instance of the Hamiltonian Path problem consisting of a graph G =(V, E) as
the input can be converted to Hamiltonian Cycle problem consisting of graph G’ =
(V’, E’). We will construct the graph G’ in the following way:

 V’ = Add vertices V of the original graph G and add an additional


vertex Vnew such that all the vertices connected of the graph are
connected to this vertex. The number of vertices increases by 1, V’
=V+1.

 E’ = Add edges E of the original graph G and add new edges between
the newly added vertex and the original vertices of the graph. The
number of edges increases by the number of vertices V, that is, E’=E+V.
 Let us assume that the graph G contains a hamiltonian path covering
the V vertices of the graph starting at a random vertex say Vstart and
ending at Vend, now since we connected all the vertices to an arbitrary
new vertex Vnew in G’. We extend the original Hamiltonian Path to a
Hamiltonian Cycle by using the edges Vend to Vnew and Vnew to
Vstart respectively. The graph G’ now contains the closed cycle traversing
all vertices once.

 We assume that the graph G’ has a Hamiltonian Cycle passing through


all the vertices, inclusive of Vnew. Now to convert it to a Hamiltonian
Path, we remove the edges corresponding to the vertex Vnew in the cycle.
The resultant path will cover the vertices V of the graph and will cover
them exactly once.

Thus we can say that the graph G’ contains a Hamiltonian Cycle if graph G contains
a Hamiltonian Path. Therefore, any instance of the Hamiltonian Cycle problem can be
reduced to an instance of the Hamiltonian Path problem. Thus, the Hamiltonian
Cycle is NP-Hard. Conclusion: Since, the Hamiltonian Cycle is both, a NP-
Problem and NP-Hard. Therefore, it is a NP-Complete problem.

5. Traveling Salesman Problem (TSP)

The Traveling Salesman Problem (TSP) is one of the most famous problems in
computer science and combinatorial optimization. In the context of Design and
Analysis of Algorithms (DAA), it serves as a primary example of an NP-Hard
optimization problem, while its "Decision" version is NP-Complete.

5.1 What is the Traveling Salesman Problem?

The problem asks the following:

"Given a list of cities and the distances between each pair of cities, what is the shortest
possible route that visits each city exactly once and returns to the origin city?"

Key Components:

 Graph Representation: Cities are nodes, and paths between them are weighted
edges.

 Hamiltonian Cycle: The salesman must find a cycle that visits every vertex
once.

 Optimization: The goal is to minimize the total weight (cost/distance) of that


cycle.

5.2 Why is it NP-Complete?

To classify a problem as NP-Complete, it must satisfy two conditions:

1. NP: If someone gives you a solution (a tour), you can verify if the total weight
is less than a value $K$ in polynomial time.

2. NP-Hard: It is at least as hard as the hardest problems in NP. TSP is typically


proven NP-Hard by reducing the Hamiltonian Cycle Problem to it.

Note: The "Optimization" version (find the absolute shortest) is NP-Hard. The
"Decision" version (is there a route shorter than $L$?) is NP-Complete.

5.3 Example:

Given a set of cities and distance between every pair of cities, the problem is to find
the shortest possible tour that visits every city exactly once and returns to the starting
point.

1. Distance Matrix (The Input)


The matrix below shows the cost of traveling between cities. Since this is a
symmetric TSP, the distance from A to B is the same as B to A.

From \ To A B C D

A 0 10 15 20

B 10 0 35 25

C 15 35 0 30

D 20 25 30 0

2. Brute Force Calculation

For $n=4$ cities, we fix the starting point at A. There are $(n-1)! = 3! = 6$ possible
Hamiltonian cycles.
Route
Hamiltonian Cycle Calculation of Total Weight Total Cost
Index

A → B → C → D → $10 (AB) + 35 (BC) + 30 (CD) + 20


1 95
A (DA)$

A → B → D → C → $10 (AB) + 25 (BD) + 30 (DC) + 15


2 80 (Optimal)
A (CA)$

A → C → B → D → $15 (AC) + 35 (CB) + 25 (BD) + 20


3 95
A (DA)$

A → C → D → B → $15 (AC) + 30 (CD) + 25 (DB) + 10


4 80 (Optimal)
A (BA)$

A → D → B → C → $20 (AD) + 25 (DB) + 35 (BC) + 15


5 95
A (CA)$

A → D → C → B → $20 (AD) + 30 (DC) + 35 (CB) + 10


6 95
A (BA)$

Result: The minimum cost is 80, achieved by the routes A → B → D → C → A and


its reverse A → C → D → B → A.

3. State-Space Tree Visualization

In DAA, we visualize these calculations using a State-Space Tree. Each level of the
tree represents choosing the next city to visit.

 Level 0: Start at node A.

 Level 1: Choose between B, C, or D.

 Level 2: Choose from remaining unvisited cities.

 Level 3: Visit the last city and return to A.

4. Mathematical Complexity

 Brute Force: O(n!)—as seen above, we check every permutation.

 Dynamic Programming (Held-Karp): O(n^2 2^n)—uses memoization to avoid


repeating sub-calculations.
4. Solving Approaches in DAA

Since we cannot solve TSP in polynomial time for large datasets, we use different
algorithmic strategies:

Approach Method Complexity Pros/Cons

Exhaustive
Brute Force O(n!) Guaranteed optimal; slow.
Search

Dynamic Held-Karp Faster than brute force; still


O(n^2 2^n)
Programming Algorithm exponential.

Very fast; often misses the


Greedy Nearest Neighbor O(n^2)
shortest path.

MST-based (2- Guaranteed to be within 2x of


Approximation O(n^2\log n)
approx) the best result.

5. Applications

 Logistics: Routing delivery trucks or school buses.

 Manufacturing: Planning the movement of a robotic drill head to make holes in


a circuit board.

 DNA Sequencing: Ordering DNA fragments to reconstruct a strand.

You might also like