unit1 2 and 3
unit1 2 and 3
o As the complexity of tasks grows, single processors struggle to meet the growing
computational demands. Problems like weather simulations, data analysis, AI, and
machine learning require immense amounts of computing power.
o Traditional serial computing faces limitations due to clock speed, heat dissipation, and
physical constraints. Parallel computing addresses these bottlenecks by distributing
tasks across multiple processors.
o Over the years, there has been a rapid evolution in hardware, with multi-core
processors, GPUs (Graphics Processing Units), and specialized processors becoming
widely available. These advancements allow multiple computations to be done
simultaneously.
3. Data-Intensive Applications:
o Data-centric tasks, such as big data analytics, image processing, and scientific
simulations, often require parallel processing to handle vast amounts of data efficiently.
Parallel computing helps divide these tasks into smaller, independent chunks that can
be processed concurrently.
4. Real-Time Processing:
5. Cost-Efficiency:
Models of Computation:
Models of computation are theoretical frameworks used to describe how computations are performed.
They allow for the analysis of parallel algorithms and help understand the limits and potential of parallel
computing systems.
o A theoretical model used to describe parallel algorithms where multiple processors have
access to a shared memory. It assumes that all processors have the same access time to
memory, and communication happens in constant time.
o Variants of PRAM:
CREW (Concurrent Read Exclusive Write): Multiple processors can read the
same memory location, but only one processor can write to it.
CRCW (Concurrent Read Concurrent Write): Multiple processors can read and
write to the same memory location simultaneously.
o In this model, each processor has its local memory, and processors communicate
through message passing. This model is more realistic for modern parallel systems like
clusters and grid computing. Communication latency and synchronization between
processors become key factors in performance.
o A shared memory model assumes that processors share a global memory space. This is
typical of multi-core systems, where each core can access a common pool of memory.
Efficient management of shared resources and preventing conflicts like race conditions
are critical in this model.
o This model focuses on applying the same operation to large sets of data simultaneously.
It is suited for tasks such as image processing or matrix multiplication, where the same
operation (e.g., addition or multiplication) needs to be applied to multiple data
elements concurrently.
1. Speedup
Definition: Speedup is the ratio of the execution time of the sequential algorithm to the
execution time of the parallel algorithm using ppp processors.
Formula
Amdahl's Law provides insight into the limitations of parallel computing, particularly when there is a
significant portion of a program that must be executed sequentially. Even as the number of processors
increases, the speedup is constrained by the sequential portion of the task, making it critical to reduce
that fraction as much as possible to achieve the best parallel performance.
5. Communication Overhead
NC (Nick's Class): Problems that can be solved in polylogarithmic time using a parallel machine.
LogP: A model used for analyzing the performance of parallel systems by considering latency,
bandwidth, and the number of processors.
Parallel algorithms can be expressed in various ways to make them easier to implement and analyze.
These expressions help both in theoretical studies and practical development.
1. Algorithm Decomposition
Task Decomposition: The problem is broken into independent tasks that can be solved in
parallel. Each task performs a different computation.
Data Decomposition: The problem is divided into smaller pieces of data, and the same
operation is applied to each piece in parallel.
Visual Representation: These diagrams represent the flow of data between tasks or
processors, helping in the design of parallel systems by showing dependencies and
communication needs.
Shared Memory: Threads share a common memory space. Data consistency is achieved using
synchronization mechanisms like locks and semaphores.
Message Passing: Each processor has its own memory and communicates with others using
message-passing protocols (e.g., MPI).
MapReduce: A programming model for processing large datasets by breaking down tasks into
smaller "map" tasks, followed by a "reduce" phase where the results are aggregated.
Race Conditions: Occur when two or more processes access shared data simultaneously and at
least one of them modifies the data.
Synchronization Primitives: Tools like barriers, locks, semaphores, and condition variables are
used to coordinate the execution of parallel tasks and avoid conflicts.
6. Load Balancing
Goal: Ensuring that each processor has approximately the same amount of work to do, to avoid
idle processors.
Techniques:
o Static Load Balancing: Dividing work in advance, based on the known workload.
Data Locality: Ensuring that data used by a processor is kept as local as possible to
avoid long memory access times.
Reducing Synchronization Overhead: Minimizing the use of synchronization
primitives to avoid unnecessary delays in parallel execution.
UNIT -2
Matrix-Vector Multiplication
Optimized Approaches for Matrix Operations
1. Strassen’s Algorithm
4. Kannungo Matrix Multiplication (Modified Strassen’s Algorithm)
Kannungo's algorithm for matrix multiplication is a more recent and advanced variant of Strassen's
matrix multiplication algorithm, designed to reduce the recursive depth and further optimize the
multiplication process. However, it is important to note that the detailed steps for Kannungo's algorithm
aren't as widely published or standardized as Strassen's, and it may not be as commonly referenced in
mainstream linear algebra textbooks.
The core idea of Kannungo’s matrix multiplication algorithm is to optimize Strassen's approach by fine-
tuning the recursive breakdown of matrices and optimizing how the matrix elements are combined to
minimize unnecessary multiplications, especially for larger matrices.
As the algorithm is based on a form of divide-and-conquer recursion similar to Strassen’s but involves
specific optimizations, I'll explain the general idea of how Kannungo's matrix multiplication improves
upon Strassen’s algorithm in a more intuitive way:
Unit 3
In database query processing, decomposition and mapping are techniques that help in optimizing the
execution of queries and improving the overall efficiency of database systems. Let's explore both
techniques:
1. Decomposition Techniques:
Decomposition refers to breaking down a complex query into smaller, more manageable subqueries or
operations. This process helps in optimizing query execution, reducing redundancy, and facilitating
easier execution plans. There are several types of decomposition techniques:
a) Query Decomposition:
Goal: To break down a query into simpler components (subqueries) that can be processed
independently, improving the query's execution.
Example: A query like SELECT * FROM employees WHERE department = 'HR' AND salary >
50000; can be decomposed into two smaller subqueries:
b) Predicate Decomposition:
Goal: Decompose the query based on its predicates (conditions) to enable more efficient
filtering and joining.
Example: In the query SELECT * FROM orders WHERE status = 'shipped' AND order_date >
'2023-01-01';, we can decompose the predicates into two:
o status = 'shipped'
Each predicate can be processed independently, possibly using different indexing techniques.
c) Relational Decomposition:
Goal: Decompose relations (tables) into smaller parts based on the needs of the query. This
could involve decomposing relations into smaller views or breaking down a large table into
smaller, more manageable parts.
Example: In a normalized database schema, a sales table may be decomposed into separate
tables for products, customers, and transactions to optimize queries and reduce redundancy.
d) Join Decomposition:
Goal: Break down complex join operations into simpler steps. This involves determining the best
way to join tables (e.g., which tables to join first, what type of join to use, etc.).
2. Mapping Techniques:
Mapping techniques refer to how queries and data are translated or mapped from one representation
to another. This can include converting a high-level query (like SQL) into an execution plan, or mapping
data between different schemas or storage formats. Key mapping techniques include:
Goal: Map a high-level logical query (written in SQL or another declarative language) to a
physical execution plan that specifies the actual operations to be performed (like scans, joins,
sorts, etc.).
Example: A query like SELECT * FROM customers WHERE city = 'New York'; might be mapped to
a physical plan that includes a Table Scan or Index Scan on the customers table, depending on
the availability of an index on the city attribute.
Goal: Map queries to execution plans based on the cost of different operations. A cost model is
used to estimate the cost of executing various plans and select the one with the lowest cost.
Example: For a query involving multiple joins, the system might map out different join orders
and choose the one with the least estimated cost in terms of I/O and CPU usage.
Goal: Map SQL queries to specific operations in the database system. This involves translating
SQL statements into operations such as joins, selections, projections, etc., and determining the
optimal execution sequence.
Example: A query like SELECT name FROM students WHERE grade > 90; may be mapped to a
scan operation on the students table, followed by a filter operation to select the records where
grade > 90.
Goal: Map data between different database schemas or storage formats. This is especially
important when integrating data from heterogeneous systems (e.g., NoSQL and relational
databases).
Example: In ETL (Extract, Transform, Load) processes, data from a relational database might be
mapped to a NoSQL format, translating fields and structures between systems.
Goal: Map queries to an execution strategy, determining how various operations like joins,
filters, and sorts will be performed.
Example: For the SQL query SELECT name FROM employees WHERE department = 'HR' ORDER
BY salary;, the database might choose to:
Improved Performance: Both techniques help optimize query execution by breaking down
complex queries into simpler components that can be more efficiently executed.
Scalability: Query decomposition helps in processing large datasets in a more manageable way,
reducing memory and processing overhead.
Flexibility: Mapping allows the database system to adapt to different storage and execution
strategies, making it flexible and adaptive to different environments.
Summary:
Decomposition and mapping techniques are integral to efficient query processing in databases.
Decomposition simplifies complex queries into smaller, more manageable subqueries, while mapping
translates high-level queries into physical execution plans, optimizing performance. Combining both
helps in designing scalable and efficient database systems.
Example Query:
FROM employees
Decomposition refers to breaking the query into smaller, more manageable parts for efficient
processing. This includes breaking down complex queries into subqueries, predicates, and operations.
a) Query Decomposition:
Goal: Break the query into simpler components that can be processed independently.
For the above query, we can break it down into the following steps:
1. Step 1: Filter employees who are in the 'Sales' department:
FROM employees
FROM employees
FROM employees
Each of these components (filtering by department, filtering by salary, and sorting) can be executed
independently, and the results can be combined to produce the final result.
b) Predicate Decomposition:
1. department = 'Sales'
We can handle these predicates separately, each having its filtering operation on the employees table.
By breaking down the conditions into separate filtering steps, the system can apply indexes more
effectively if available.
c) Join Decomposition:
If there were joins involved, this would involve breaking down a complex join operation into smaller
subqueries or steps. For example, if we had to join the employees table with a departments table, we
would decompose the join into smaller, more manageable parts, such as first joining employees with
departments and then filtering by conditions like salary > 50000.
2. Mapping Techniques in Query Processing
Mapping refers to translating high-level queries into physical operations and plans that the database can
execute efficiently.
Goal: Map the logical query into a physical execution plan that the database can execute. The
physical execution plan specifies how the database will retrieve and process the data.
FROM employees
The logical query is the one that we write in SQL, which is:
Select employees from the employees table where department = 'Sales' and salary > 50000.
1. Step 1: Perform a table scan (or index scan, if an index on department exists) on the employees
table to filter the rows where department = 'Sales' and salary > 50000.
2. Step 2: Perform a sort operation to arrange the results in descending order by salary.
3. Step 3: Return the selected columns: employee_id, name, department, and salary.
The system may use indexing on salary or department to make the filtering process more efficient,
depending on the available indexes.
Goal: Map the query to an execution plan that minimizes the cost (e.g., I/O operations, CPU
time).
Let's say the system has multiple ways to execute the query. The database's query optimizer will
estimate the cost of various execution plans and choose the one with the lowest cost.
2. Table scan followed by filtering: If there is no relevant index, the database might choose a full
scan of the employees table followed by filtering.
The optimizer uses statistics (like the number of rows in the table and the distribution of values in
columns) to calculate the cost of each plan.
If the employees table in a relational database is being transferred to a NoSQL database or another
schema, data mapping techniques would ensure that the data is transformed into the appropriate
format. For example:
The relational employee_id, name, salary fields might be stored as documents in a NoSQL
system, and the data types might need conversion (e.g., converting integers or dates to JSON-
compatible formats).
1. Step 1: Apply a filter on department = 'Sales' and salary > 50000 using an index or a table scan.
3. Step 3: Select the employee_id, name, department, and salary columns from the filtered and
sorted result.
If there are indexes on department or salary, the database might choose to use an Index Scan for faster
filtering. If no indexes are available, the database may use a Full Table Scan followed by the necessary
filtering and sorting.
Decomposition: The original query was broken down into smaller parts, such as filtering
employees by department, filtering by salary, and sorting the results.
Mapping: The system translated the SQL query into an execution plan, optimizing the query's
physical execution. It considered the availability of indexes, the cost of operations, and the best
execution strategy (e.g., sorting first or filtering first).
By combining decomposition (breaking the query into simpler components) and mapping (mapping the
query into a physical plan), the system can execute queries efficiently and optimize performance based
on the available resources.
FROM products
This query retrieves the product name, price, and category from a products table, joining it with a
categories table. It filters the products to include only those in the "Electronics" category and whose
price is greater than 100. Finally, it orders the result by price in ascending order.
a) Query Decomposition:
1. Step 1: Perform the Join We start by joining the products table with the categories table using
category_id:
FROM products
At this stage, we have all the products with their corresponding categories, but we haven't yet applied
the filtering conditions.
2. Step 2: Apply the Filters After performing the join, we apply the filter condition category_name
= 'Electronics' and price > 100:
Now, we have a subset of products that are both in the "Electronics" category and priced above 100.
3. Step 3: Perform the Sorting Finally, we order the results by the price column in ascending order:
FROM products
This step ensures that the output is sorted by price from lowest to highest.
b) Predicate Decomposition:
1. categories.category_name = 'Electronics'
We can process these conditions separately during execution. The filtering condition on category_name
can be applied during the join operation or as a separate step, and the filtering condition on price can
be applied after that.
Goal: Map the logical query into a physical execution plan that the database can execute.
3. Sorting by price.
o Method: The database can either use a Nested Loop Join, Merge Join, or Hash Join
depending on the available indexes and table sizes.
o Example: If the category_id column is indexed, the system may choose a Hash Join to
quickly find matching rows from both tables.
2. Step 2: Filter the rows where category_name = 'Electronics' and price > 100:
o Method: The system could use Index Scan on categories.category_name and Index Scan
or Table Scan on products.price (depending on available indexes).
o The filters can be applied after the join or as part of the join, depending on the system's
optimization choices.
o Method: Once the results are filtered, the database performs an In-Memory Sort or
uses an External Merge Sort if the result set is large.
The database will optimize the execution plan to minimize the total cost of disk I/O, CPU usage, and
memory consumption.
Goal: Optimize the query's execution by estimating the cost of different strategies and selecting the one
with the lowest cost.
For the given query, the database will analyze different execution strategies and choose the most
efficient one. This can involve:
Index Usage: If the category_name and price columns have indexes, the optimizer may choose
to use these indexes during filtering, avoiding full table scans.
Join Order: If the categories table is smaller than the products table, the optimizer may choose
to scan categories first and then perform the join, reducing the number of rows involved in the
join.
Join Method: Depending on the size of the tables and available indexes, the optimizer may
choose a Hash Join or Nested Loop Join.
Goal: Map data from one schema to another, especially when dealing with different database systems.
1. Relational Database (SQL-based): The query is originally executed on a relational database with
tables products and categories.
2. NoSQL Database: Suppose the data is also stored in a NoSQL system, where the products and
categories are stored in document format, with the category_id nested within each product
document.
To map the data from the relational schema to NoSQL, the system might need to flatten the structure,
transforming the products and categories relationship into a format suitable for NoSQL storage. This
could involve creating a product document that directly includes category information (e.g., the product
document might include a category_name field as part of each product document).
2. Step 2: Hash Join between the products and categories tables based on category_id, as both
tables may be large, and the optimizer chooses this method for efficient joining.
3. Step 3: Index Scan or Table Scan on products where price > 100, depending on the available
index on the price column.
5. Step 5: Return the result: product_name, price, and category_name for products that match the
conditions.
Summary:
In this example:
Decomposition breaks down the query into smaller components, such as the join, filtering by
category and price, and sorting.
Mapping refers to translating this high-level query into a physical execution plan that includes
operations like joins, scans, filtering, and sorting, and selecting the best strategy based on cost.
By optimizing both the decomposition and mapping processes, the database can execute the query in a
more efficient manner, improving performance and reducing resource consumption.
15 puzzle problem
The 15-puzzle problem (also called the sliding puzzle) is a well-known problem in artificial intelligence
and puzzle games. It consists of a 4x4 grid of numbered tiles, where the goal is to arrange the tiles in a
specific order by sliding them around within the grid.
Problem Setup:
The puzzle consists of 15 numbered tiles (1 through 15) and one empty space. The empty space
allows you to move tiles into it, thus "sliding" them around.
The goal is to arrange the tiles in numerical order, with the empty space at the bottom-right
corner of the grid.
Initial Configuration:
In the example above, the _ represents the empty space, and the goal is to arrange the tiles so that they
appear in order from 1 to 15, with the empty space in the bottom-right corner.
Legal Moves:
The legal moves involve sliding one tile at a time into the empty space. For example:
If the empty space is adjacent to a tile (up, down, left, or right), that tile can be moved into the
empty space.
The tiles can slide up, down, left, or right, depending on the position of the empty space.
Solved Configuration:
1. Manual Solution: Involves thinking through the puzzle step-by-step, focusing on small parts of
the puzzle and slowly moving pieces to their correct positions.
2. Breadth-First Search (BFS): A complete search algorithm that explores all possible
configurations level by level, guaranteeing the shortest solution. However, this is
computationally expensive.
3. A Search Algorithm:* This heuristic search algorithm is commonly used for solving sliding
puzzles. It uses an evaluation function, typically based on a heuristic like the Manhattan
distance, which estimates the number of moves to the goal state.
For each tile, the Manhattan distance is the sum of the horizontal and vertical distances from its current
position to its goal position. The goal of the A* algorithm is to minimize this distance and reach the goal
configuration.
The A search algorithm* is an informed search algorithm that combines the benefits of Dijkstra's
algorithm and greedy search. It uses a heuristic to guide the search towards the goal efficiently. In the
case of the 15-puzzle, the goal is to move the tiles into a specific order, and A* will find the optimal
solution by evaluating possible configurations based on their cost (number of moves so far) and
heuristic estimate of how far the goal is.
2. Heuristic Function: Define a heuristic to estimate how far a given state is from the goal.
3. Priority Queue: Use a priority queue to select the state with the least estimated total cost to
explore.
4. Goal Test: The search stops when the goal configuration is reached.
1. State Representation
Each state in the 15-puzzle can be represented as a 4x4 grid, where each tile is numbered 1 to 15 and
one position is empty (represented by 0). The state also includes:
In this state, the empty space is at position (3,2), which is the third row and second column.
2. Heuristic Function
The heuristic function (denoted as h(n)) is a way to estimate the "distance" of a given state from the
goal. A common heuristic for sliding puzzles is the Manhattan distance, which is the sum of the
horizontal and vertical distances of each tile from its target position in the goal configuration.
For example, if the tile 15 is in the position (3,3) in the current state but should be in (3,4) in the goal
state, the Manhattan distance for this tile would be 1 (moving it one position to the right).
For the entire puzzle, the heuristic function h(n) is calculated by summing the Manhattan distances for
each tile.
3. A Search Process*
o g(n) is the cost to reach the current state (the number of moves made so far).
o h(n) is the heuristic estimate of the cost from the current state to the goal state.
The algorithm explores the states with the lowest f(n) first, ensuring the most promising states are
expanded first.
4. Algorithm Execution
Let's walk through an example of solving the 15-puzzle using A* search.
Initial Configuration:
For the initial configuration, we calculate the Manhattan distance for each tile:
The empty space (0) does not contribute to the Manhattan distance but will affect the moves. The sum
of the Manhattan distances for this configuration might be some value h(n).
From the current state, we generate possible moves by sliding a tile into the empty space. The valid
moves are based on the current position of the empty space.
Valid moves: Slide tiles adjacent to the empty space into the empty spot (up, down, left, right).
After generating all possible moves (successor states), we calculate the cost (f(n) = g(n) + h(n)) for each
new state and push them into the priority queue. The state with the smallest f(n) is expanded first.
Step 4: Repeat
We continue expanding the states, calculating the cost function for each, until we find the goal
configuration. The algorithm explores the most promising configurations based on both the cost so far
(g(n)) and the heuristic estimate (h(n)).
Let's start with an example using simplified steps to demonstrate A* search. We'll use the following
configurations:
Goal Configuration:
Initial Configuration:
Tile 10: Current position (2,1), goal position (2,1), Manhattan distance = 0
Tile 11: Current position (2,2), goal position (2,2), Manhattan distance = 0
Tile 12: Current position (2,3), goal position (2,3), Manhattan distance = 0
Tile 13: Current position (3,0), goal position (3,0), Manhattan distance = 0
Tile 14: Current position (3,1), goal position (3,1), Manhattan distance = 0
Tile 15: Current position (3,3), goal position (3,2), Manhattan distance = 1 (1 step left)
Empty space (0): Current position (3,2), goal position (3,3), Manhattan distance = 1 (1 step right)
After Move 1:
New configuration:
Tile 10: Current position (2,1), goal position (2,1), Manhattan distance = 0
Tile 11: Current position (2,2), goal position (2,2), Manhattan distance = 0
Tile 12: Current position (2,3), goal position (2,3), Manhattan distance = 0
Tile 13: Current position (3,0), goal position (3,0), Manhattan distance = 0
Tile 14: Current position (3,1), goal position (3,1), Manhattan distance = 0
Tile 15: Current position (3,3), goal position (3,2), Manhattan distance = 1
Empty space (0): Current position (3,2), goal position (3,3), Manhattan distance = 1
After Move 2:
New configuration:
Tile 10: Current position (2,1), goal position (2,1), Manhattan distance = 0
Tile 11: Current position (2,2), goal position (2,2), Manhattan distance = 0
Tile 12: Current position (2,3), goal position (2,3), Manhattan distance = 0
Tile 13: Current position (3,0), goal position (3,0), Manhattan distance = 0
Tile 14: Current position (3,1), goal position (3,1), Manhattan distance = 0
Tile 15: Current position (3,3), goal position (3,2), Manhattan distance = 1
Empty space (0): Current position (3,1), goal position (3,3), Manhattan distance = 2
After Move 3:
New configuration:
Tile 10: Current position (2,1), goal position (2,1), Manhattan distance = 0
Tile 11: Current position (2,2), goal position (2,2), Manhattan distance = 0
Tile 12: Current position (2,3), goal position (2,3), Manhattan distance = 0
Tile 13: Current position (3,2), goal position (3,0), Manhattan distance = 2 (need to move up 2)
Tile 14: Current position (3,1), goal position (3,1), Manhattan distance = 0
Tile 15: Current position (3,3), goal position (3,2), Manhattan distance = 1
Empty space (0): Current position (3,0), goal position (3,3), Manhattan distance = 3
Total Manhattan distance after Move 3 = 2 (Tile 13) + 1 (Tile 15) + 3 (Empty space) = 6
After Move 4:
New configuration:
Copy
1 2 3 4
5 6 7 8
9 10 11 0
_ 14 13 15
Tile 11: Current position (2,2), goal position (2,2), Manhattan distance = 0
Tile 12: Current position (2,3), goal position (2,3), Manhattan distance = 0
Tile 13: Current position (3,2), goal position (3,0), Manhattan distance = 2
Tile 14: Current position (3,1), goal position (3,1), Manhattan distance = 0
Tile 15: Current position (3,3), goal position (3,2), Manhattan distance = 1
Empty space (0): Current position (2,3), goal position (3,3), Manhattan distance = 1
Total Manhattan distance after Move 4 = 2 (Tile 13) + 1 (Tile 15) + 1 (Empty space) =4
After all the steps, when we arrive at the goal configuration, the Manhattan distance will be 0 because
all tiles are in their correct positions.
The 15-puzzle problem is not just a recreational game but has many practical applications in computer
science and artificial intelligence. Here are some key areas where the 15-puzzle problem is applied:
The 15-puzzle problem is often used as a benchmark for testing and evaluating search algorithms in AI.
The challenge of solving the puzzle using different strategies, like A search, depth-first search (DFS),* and
breadth-first search (BFS), provides a way to study how AI can find the most efficient solution in a state
space with many possibilities.
2. Pathfinding Algorithms:
Solving the 15-puzzle is similar to pathfinding in robotics, autonomous vehicles, and game
development. In these fields, we often need to determine the best path or action to move from one
state (configuration) to another, much like how a piece needs to be moved in the puzzle. The techniques
from solving the 15-puzzle (like A* search or BFS) are widely applicable to pathfinding in various
domains.
Example Application: Many mobile puzzle games, such as sliding tile puzzles or memory games,
use principles from the 15-puzzle to create challenging and engaging gameplay mechanics.
4. Computer Vision:
In computer vision, especially in image segmentation and image processing, the 15-puzzle can be
related to problems where an image or grid needs to be reconstructed or rearranged. The puzzle’s
nature of moving tiles and restoring a specific order has similarities with tasks like image restoration or
reordering data based on specific constraints.
The concept of rearranging and shifting pieces within the 15-puzzle can have applications in
cryptography, where encryption and decryption methods often involve permutations and
transformations of data. The puzzle can be used to understand how to transform data while ensuring
that it can be restored correctly (solved).
Example Application: Certain permutation ciphers in cryptography can use tile-like shifts and
transformations to scramble and unscramble data, similar to solving the 15-puzzle.
In parallel computing, solving the 15-puzzle can be an example of how multiple agents or processors can
work together to solve a problem. The challenge lies in dividing the puzzle-solving task into smaller sub-
tasks, such as evaluating moves or calculating heuristics, and distributing them across multiple
computing units for efficiency.
Example Application: In a distributed system, you could have multiple agents working in parallel
to explore the puzzle's state space, optimizing for the solution faster than a single processor
could manage.
7. Optimization Problems:
The 15-puzzle is an example of a combinatorial optimization problem, where the goal is to find the best
solution (the goal configuration) from a set of possible configurations by optimizing for a specific
criterion (such as the minimum number of moves). Solving such problems is key in fields like operations
research and logistics.
Example Application: In logistics, managing the optimal arrangement of goods in a warehouse
or a delivery system can involve similar optimization techniques to the ones used in the 15-
puzzle.
Here's a Python implementation of the 15-puzzle using the A Search Algorithm*. In this example, the
goal is to find the sequence of moves that transforms the initial configuration of the puzzle into the goal
configuration. The program uses the Manhattan distance as the heuristic to guide the search.
import heapq
[5, 6, 7, 8],
def find_empty_space(state):
for i in range(4):
for j in range(4):
if state[i][j] == 0:
return i, j
def is_goal_state(state):
moves = []
if i > 0: # Move up
moves.append((-1, 0))
moves.append((1, 0))
moves.append((0, -1))
moves.append((0, 1))
return moves
return new_state
def manhattan_distance(state):
distance = 0
for i in range(4):
for j in range(4):
if state[i][j] != 0:
target_i, target_j = (state[i][j] - 1) // 4, (state[i][j] - 1) % 4
return distance
def a_star_search(start_state):
open_list = []
closed_list = set()
while open_list:
if is_goal_state(current_state):
return path
h = manhattan_distance(new_state)
def print_state(state):
print()
def main():
[5, 6, 7, 8],
print("Start state:")
print_state(start_state)
solution_path = a_star_search(start_state)
if solution_path:
print("Solution found!")
else:
if __name__ == "__main__":
main()
1. Goal State: We define the goal configuration of the puzzle where the empty space is at the
bottom-right (represented by 0).
2. find_empty_space: This function finds the position of the empty space (represented by 0) in the
current state.
3. is_goal_state: This checks if the current state matches the goal state.
4. get_possible_moves: This function calculates the possible moves of the empty space based on
its position. The empty space can move up, down, left, or right, if those moves are within the
bounds of the puzzle.
5. move: This function generates a new state by swapping the empty space with an adjacent tile.
6. manhattan_distance: This function calculates the Manhattan distance, which is the sum of the
vertical and horizontal distances of each tile from its current position to its goal position.
7. a_star_search: This is the main function implementing the A* search algorithm. It keeps track of
the states in a priority queue and explores the states with the lowest f = g + h value, where:
o g is the cost to reach the current state (number of moves made so far).
8. print_state: This function prints the current state of the puzzle in a readable format.
3. If a solution is found, it will print the path (the sequence of moves the empty space takes).
4. If no solution is found (though for the 15-puzzle problem, there is always a solution if the puzzle
is solvable), it will print a message stating so.
Sample Output:
Start state:
1234
5678
9 10 11 12
13 14 15
Solution found!
Solution path (moves of the empty space): [(3, 2), (3, 1), (3, 0), (2, 0), (1, 0), (0, 0), ...]
This output shows the solution path for the empty space to reach the goal configuration. Each tuple
represents a move where the empty space shifts to a new position.
1. Discrete Events: These are events that happen at a specific point in time, such as the arrival of a
message, a machine breaking down, or a train reaching a station.
2. Event List: This is a queue that contains all the scheduled events, sorted by time.
3. Logical Processes (LPs): These are the independent entities responsible for processing events.
Each LP may simulate different parts of the system, such as different subsystems or
components.
4. Synchronization: One of the critical challenges of PDES is synchronizing events across the
different logical processes to ensure a consistent simulation.
5. Time Management: In PDES, time must be managed in a way that ensures that events occur in
the correct order. This can be challenging when different LPs may process events at different
times, which could cause inconsistencies.
1. Event Scheduling: Each LP schedules events that it will process in the future. These events are
stored in the event list (or priority queue).
2. Event Processing: LPs process the events according to their timestamps, and then generate new
events as required.
3. Synchronization of Events: After processing an event, the simulation checks if there are any
dependencies between LPs that must be synchronized. If necessary, events from other LPs are
brought into the simulation, ensuring consistency.
Let's take the example of simulating a simple manufacturing system where multiple machines are
involved, each processing different products. We want to simulate this system in parallel, where each
machine can process products independently, but we need to synchronize certain events like when a
product finishes processing.
Events: Each machine processes a product, and when a machine finishes, it generates an event
that a product is ready for the next stage.
Synchronization: When the last product is processed, we need to know that all machines have
finished processing before ending the simulation.
Steps to Simulate:
1. Initialization: We create the event list, which contains the start time of each machine's first
event.
2. Event Scheduling: Each machine schedules an event for when it will finish processing a product.
3. Parallel Processing: Machines process their events in parallel. If the machines are synchronized,
they must check each other’s event list to ensure the events are processed in the correct order.
4. Synchronization Point: At the end of the simulation, we synchronize all the machines to check if
they have completed their tasks.
In this simple example, we'll simulate a manufacturing system with two machines. The machines will
process products in parallel, but we need to synchronize them to print when all events are processed.
import random
import time
import concurrent.futures
class Machine:
self.name = name
time.sleep(self.time_to_process)
return finish_time
return machine.process_event(event_time)
def main():
# Create machines
initial_event_time = 0
result1 = future1.result()
result2 = future2.result()
# Print out the final event time when both machines are done
main()
1. Machine Class: Each machine has an event list, a name, and a random processing time to
simulate how long each machine takes to finish processing an item.
2. run_simulation Function: This function processes the events for a given machine by simulating
processing time with time.sleep.
3. main Function:
o The future1.result() and future2.result() methods wait for the completion of the
simulation for each machine.
Sample Output:
Parallel Processing: Machine 1 and Machine 2 are processing their events in parallel, simulating
how a real manufacturing system might work.
Synchronization: Even though the events are processed in parallel, we ensure that both
machines are synchronized by checking the completion time of the last event.
Challenges in PDES:
Synchronization Overhead: As the number of logical processes increases, the overhead due to
synchronization becomes significant. Techniques like optimistic simulation or conservative
synchronization are used to minimize these costs.
Event Causality: It's important to respect the causality of events (i.e., events must be processed
in the correct order to ensure the correctness of the simulation).
Real-World Applications of PDES:
1. Distributed Systems Simulation: Simulating large-scale distributed systems where each node or
component processes events in parallel.
2. Network Simulation: PDES is used to model networks where multiple routers or servers process
data packets in parallel.
3. Traffic Simulation: Simulating traffic flow with multiple intersections or roads where each road
is processed independently but needs synchronization for events like car arrivals and traffic light
changes.
By utilizing PDES, simulations that would otherwise take too long to run sequentially can be completed
in a much shorter time by exploiting parallelism.
Image Dithering
Image Dithering is a technique used to create the illusion of color depth in images with a limited color
palette. It is especially useful in reducing the number of colors used in an image while trying to maintain
the visual quality and texture of the original image.
What is Dithering?
When an image is displayed on a device with a limited color palette (such as old computer monitors,
printers, or systems with limited color depth), dithering helps to simulate the appearance of a broader
range of colors by strategically placing pixels of the available colors next to each other. This creates the
perception of intermediate colors or shades, which would otherwise be unavailable due to the color
limitations.
Ordered Dithering
Floyd-Steinberg Dithering
Atkinson Dithering
For example:
If you have a pixel that should be light gray, but your device can only show either black or white,
dithering might place black and white pixels near each other to simulate gray.
1. Ordered Dithering:
Ordered dithering uses a fixed matrix to decide where to place the dithered colors. It is a simple and fast
method but may not produce the best results in terms of visual quality.
Example: You might use a matrix like the Bayer matrix, which breaks the image into smaller blocks and
applies dithering based on the intensity of each pixel.
0 8 2 10
12 4 14 6
3 11 1 9
15 7 13 5
In ordered dithering, you would compare each pixel with the threshold value in the matrix and decide
whether it should be light or dark based on whether it exceeds the threshold.
2. Floyd-Steinberg Dithering:
This is one of the most widely used dithering techniques. It is a error diffusion method, where the error
(difference between the actual color and the closest available color) is distributed to neighboring pixels.
This gives a more natural appearance and works well for photographs or images with continuous tones.
Steps:
Start with the top-left pixel and calculate the color difference (error) between the pixel’s color
and the closest available color.
Distribute this error to the neighboring pixels (to the right, bottom-right, and bottom pixels).
new_pixel = round(original_pixel)
pixel(x+1, y) += error * 7 / 16
This technique is more computationally expensive than ordered dithering but gives better results.
3. Atkinson Dithering:
Atkinson Dithering is a simplified version of error diffusion that uses a smaller, simpler matrix. It is often
faster than Floyd-Steinberg but produces a different visual effect. It diffuses the error to fewer
neighboring pixels, which results in a less detailed but still effective dither pattern.
new_pixel = round(original_pixel)
pixel(x+1, y) += error * 1 / 8
pixel(x+2, y) += error * 1 / 8
Here’s a Python example to demonstrate Floyd-Steinberg dithering using the PIL (Pillow) library for a
grayscale image:
from PIL import Image
import numpy as np
def floyd_steinberg_dither(image):
img = image.convert('L')
pixels = np.array(img)
dithered_image = pixels.copy()
for y in range(height):
for x in range(width):
old_pixel = dithered_image[y, x]
dithered_image[y, x] = new_pixel
dithered_image[y, x + 1] += error * 7 / 16
dithered_image[y + 1, x - 1] += error * 3 / 16
if y + 1 < height:
dithered_image[y + 1, x] += error * 5 / 16
dithered_image[y + 1, x + 1] += error * 1 / 16
return Image.fromarray(dithered_image.astype(np.uint8))
# Load an image
image = Image.open("example_image.jpg")
dithered_image = floyd_steinberg_dither(image)
image.show(title="Original Image")
dithered_image.show(title="Dithered Image")
Explanation:
The function floyd_steinberg_dither() takes a grayscale image, converts it to a numpy array, and
processes each pixel to apply the Floyd-Steinberg dithering.
The error is diffused to neighboring pixels, resulting in a dithered image that simulates a more
continuous range of gray levels using only black and white.
Ordered Dithering Explained in Detail
Ordered dithering is a process that uses a threshold matrix to decide whether each pixel in the
image should be light or dark. This technique helps create the illusion of more colors in an image
with a limited color palette, usually for systems that can only display black and white or very few
colors.
The process involves comparing the pixel values (typically grayscale intensities) to corresponding
values in a pre-defined threshold matrix. If the pixel value is greater than the threshold value at that
position in the matrix, it is set to white (or light); otherwise, it is set to black (or dark).
Key Concept:
In ordered dithering, a matrix (often called a threshold matrix) is applied repeatedly across the
image in a tiled manner. The matrix values determine the thresholds that the image's pixel intensity
is compared against. If the pixel value exceeds the matrix value, the pixel is set to white; otherwise,
it is set to black.
This matrix is a common threshold matrix used for ordered dithering. It is based on a simple pattern,
but it can be scaled up for higher color depths.
0 8 2 10
12 4 14 6
3 11 1 9
15 7 13 5
1. Convert the image to grayscale: If the image is in color, we first convert it to grayscale because
dithering is typically applied to grayscale images.
2. Normalize the pixel values: We scale the pixel values to a range of 0 to 15, assuming we are
using a 4-level grayscale. For a more complex image, we could scale the pixel values accordingly
to fit the threshold matrix size.
3. Apply the Bayer matrix: The Bayer matrix is tiled over the image. Each pixel in the image is
compared with the corresponding value in the matrix. If the pixel value is greater than the
matrix value at that position, the pixel is set to light (white); if it's smaller, it is set to dark (black).
4. Tile the matrix: The Bayer matrix is tiled across the image, so you start again with the first value
of the matrix once you reach the end.
Detailed Example:
Let’s walk through a simple image (a 4x4 grayscale image) and apply ordered dithering using the 4x4
Bayer matrix.
Assume we have the following grayscale image where each value represents the intensity of the
pixel (range 0 to 15, where 0 is black and 15 is white):
3 10 5 12
15 8 6 2
9 4 14 7
1 11 13 0
o In this example, we already have pixel values in the range of 0 to 15. If the pixel values
were in a different range (e.g., 0 to 255), we would normalize them to fit within this
range.
0 8 2 10
12 4 14 6
3 11 1 9
15 7 13 5
3. Tile the Bayer matrix: For a 4x4 image, we apply the Bayer matrix directly to each pixel, and
then repeat it as needed (it fits exactly in a 4x4 grid, so no tiling is necessary here).
4. Compare pixel values with the threshold matrix: For each pixel (x, y) in the image, compare the
pixel value with the corresponding threshold value in the matrix:
After applying the ordered dithering using the Bayer matrix, the result looks like this:
markdown
255 255 255 255
255 255 0 0
255 0 255 0
0 255 255 0
In the output image, black pixels (0) and white pixels (255) are arranged in a way that simulates
intermediate shades of gray when viewed from a distance. This pattern simulates a grayscale image
using only two colors (black and white), but because of the dithering pattern, the result looks like it
has more gray shades.
Key Points:
1. Threshold Matrix: The Bayer matrix defines the threshold for each pixel, and the pixel is set to
white if it exceeds the threshold value and to black if it doesn't.
2. Tiling: The threshold matrix is tiled across the image. If the matrix is smaller than the image, it
repeats in a grid pattern.
3. Illusion of Gray: By arranging black and white pixels in this way, ordered dithering creates the
illusion of intermediate grays or colors, even if only two colors (black and white) are available.
Ordered dithering is a fast and simple method for simulating a broader color range on devices with
limited color depth. However, it can create noticeable patterns in the image, which can be visible if
the image is viewed closely.
Floyd-Steinberg Dithering:
Floyd-Steinberg Dithering is an error diffusion technique. The basic idea is to adjust the color of the
current pixel and then "spread" the error (the difference between the original and the new color) to
the neighboring pixels. The diffusion is done in specific directions (to the right, bottom-right,
bottom, and bottom-left pixels).
New Pixel = Rounded Value of the Pixel to the nearest available color (typically 0 or 255 for
black and white)
Then, the error is distributed as follows:
Pixel (x + 1, y) += Error * 7 / 16
Pixel (x - 1, y + 1) += Error * 3 / 16
Pixel (x + 1, y + 1) += Error * 1 / 16
Let’s walk through the same 4x4 grayscale image example and apply Floyd-Steinberg dithering.
3 10 5 12
15 8 6 2
9 4 14 7
1 11 13 0
We process the image pixel by pixel, from left to right, top to bottom. After each pixel is processed,
we adjust the error and diffuse it to neighboring pixels.
Updated image:
0 10 5 12
16.31 8 6 2
9 4 14 7
1 11 13 0
And so on. The algorithm continues this way across all pixels in the image.
Atkinson Dithering:
Atkinson Dithering is another error diffusion technique that is simpler and less computationally
expensive than Floyd-Steinberg. In Atkinson dithering, the error is diffused to fewer neighboring
pixels, which makes the result less detailed but still effective.
New Pixel = Rounded Value of the Pixel to the nearest available color (typically 0 or 255 for
black and white)
Pixel (x + 1, y) += Error * 1 / 8
Pixel (x + 2, y) += Error * 1 / 8
Pixel (x + 1, y + 1) += Error * 1 / 8
Pixel (x + 1, y + 2) += Error * 1 / 8
3 10 5 12
15 8 6 2
9 4 14 7
1 11 13 0
We process each pixel from left to right, top to bottom. The error is diffused in the directions
defined by the Atkinson formula.
Updated image:
0 10 5 12
15.375 8 6 2
9.375 4 14 7
1 11 13 0
And so on. The algorithm continues this way across all pixels in the image.
Floyd-Steinberg Dithering: This method diffuses the error to four neighboring pixels (right,
bottom-right, bottom, bottom-left) and gives excellent results, especially for continuous-tone
images. It requires more computation than ordered or Atkinson dithering but provides more
precise and visually appealing results.
Atkinson Dithering: This method diffuses the error to fewer neighboring pixels, making it
computationally simpler but potentially less accurate. It's a good choice for quick dithering
where a high level of detail isn't necessary.
Both techniques allow you to simulate grayscale images with a limited palette, though the result and
computational cost can vary.
Display on Low-Color Devices: Dithering is particularly useful when displaying images on devices
that have a limited color palette (e.g., old monitors, mobile screens).
Image Compression: Dithering can be used to compress images by reducing the number of
colors, while still preserving visual detail.
Printed Graphics: When printing images with limited ink colors, dithering can help simulate
continuous tones and shades.
Dense LU factorization
Some of the notable techniques for LU decomposition include:
1. Crout’s Method
Doolittle’s Method
Doolittle’s method is another variant of LU decomposition where the matrix A is decomposed into:
LDU Decomposition
Block LU decomposition is used for large matrices that are too large to fit into memory efficiently. The
idea is to divide the matrix into smaller blocks and perform LU decomposition on each block, rather than
performing decomposition on the entire matrix at once.
This technique is particularly useful when dealing with block matrices or sparse matrices where matrix
entries are grouped into blocks.
Steps:
Perform LU decomposition on the blocks, and then combine the results to get the overall LU
decomposition.
Block LU decomposition can be much faster for large-scale problems when parallelized or optimized for
specific hardware architectures, such as in parallel computing or GPU-based computation.
Step-by-Step Process for Dense LU Factorization