|
Last updated on:
10 November 2008 06:01:31 PM
1. Techniques for parsing input
data
2. Complete Search a.k.a Brute
Force
3. Recursion / Backtracking plus Branch
and Bound
4. Divide and Conquer / Transform and
Conquer?
5.
Optimizing your source code
7.
Some solution paradigms
8.
Overview on more specialized algorithms: mathematical, sorting,
searching, greedy, dynamic programming, graph-related, computational
geometry.
1.
Techniques for parsing input
data
In all programming contest,
or more specifically, in all useful program, you need to read in input and
process it. However, the input data can be as nasty as possible, and this
can be very troublesome to parse.
If you spent too much time
in coding how to parse the input efficiently and you are using C/C++ as
your programming language, then this tip is for you. Let's see an example:
How to read the following
input:
There are N lines, each lines
always start with character '0' followed by '.', then unknown number of
digits x, finally the line always terminated by three dots "...".
N
0.xxxx...
The fastest way to do it is:
#include <stdio.h>
char digits[100];
void main() {
scanf("%d",&N);
for (i=0; i<N; i++) {
scanf("0.%[0-9]...",&digits); // surprised?
printf("the digits are 0.%s\n",digits);
}
}
Yeah, this is the trick that
many C/C++ programmers doesn't aware of. Why I said C++ while scanf/printf
is a standard C I/O routines? This is because many C++ programmers "forcing"
themselves to use cin/cout all the time, without realizing that scanf/printf
can still be used inside all C++ programs. Sadly, scanf/printf is declared
as deprecated in the newest version of Microsoft Visual C++. I don't know
why...
Mastery of programming language
I/O routines will help you a lot in programming contests.
References:
-. Your programming language manuals :)
2.
Complete Search a.k.a Brute
Force
Keep It Short and Simple
Taken
from USACO Training Gateway section 1.1.2
This is the
most basic problem solving technique. Utilizing the fact that computer is
actually very fast...
Solving a
problem using complete search is based on the 'Keep It Simple, Stupid' /
'Keep It Short & Simple' (abbreviated as KISS) principle. The goal of solving
contest problems is to write programs that work in the time allowed, whether
or not there is a faster algorithm.
Complete search
exploits the brute force, straight-forward, try-them-all method of finding
the answer. This method should almost always be the first algorithm/solution
you consider. If this works within time and space constraints, then do it:
it's easy to code and usually easy to debug. This means you'll have more
time to work on all the hard problems, where brute force doesn't work quickly
enough.
In the case
of a problem with only fewer than a couple million possibilities, iterate
through each one of them, and see if the answer works.
Careful, Careful
Sometimes,
it's not obvious that you use this methodology. Let's see the following
examples to get the feeling of doing things in "brute force"
Party Lamps
[IOI 98]
You are given
N lamps and four switches. The first switch toggles all lamps, the second
the even lamps, the third the odd lamps, and last switch toggles lamps 1,4,7,10,...
Given the
number of lamps, N, the number of button presses made (up to 10,000), and
the state of some of the lamps (e.g., lamp 7 is off), output all the possible
states the lamps could be in.
Naively, for
each button press, you have to try 4 possibilities, for a total of 4^10000
(about 10^6020), which means there's no way you could do complete search
(this particular algorithm would exploit recursion).
Noticing that
the order of the button presses does not matter gets this number down to
about 10000^4 (about 10^16), still too big to completely search (but certainly
closer by a factor of over 10^6000).
However, pressing
a button twice is the same as pressing the button no times, so all you really
have to check is pressing each button either 0 or 1 times. That's only 2^4
= 16 possibilities, surely a number of iterations solvable within the time
limit.
The Clocks
[IOI 94]
A group of
nine clocks inhabits a 3 x 3 grid; each is set to 12:00, 3:00, 6:00, or
9:00. Your goal is to manipulate them all to read 12:00. Unfortunately,
the only way you can manipulate the clocks is by one of nine different types
of move, each one of which rotates a certain subset of the clocks 90 degrees
clockwise.
Find the shortest
sequence of moves which returns all the clocks to 12:00.
The 'obvious'
thing to do is a recursive solution, which checks to see if there is a solution
of 1 move, 2 moves, etc. until it finds a solution. This would take 9^k
time, where k is the number of moves. Since k might be fairly large, this
is not going to run with reasonable time constraints.
Note that
the order of the moves does not matter. This reduces the time down to k^9,
which isn't enough of an improvement.
However, since
doing each move 4 times is the same as doing it no times, you know that
no move will be done more than 3 times. Thus, there are only 49 possibilities,
which is only 262,072, which, given the rule of thumb for run-time of more
than 10,000,000 operations in a second, should work in time. The brute-force
solution, given this insight, is perfectly adequate.
It is beautiful, isn't it? If you have this
kind of reasoning ability. Many seemingly hard problems is eventually solvable
using brute force :).
References:
-. USACO training gateway
3.
Recursion
Do you realized that the statement: "I am
who I am" in Exodus 3:14 is recursive.
Recursion is cool. Many algorithms need to
be implemented recursively. A popular combinatorial brute force algorithm,
backtracking, usually implemented recursively. After highlighting it's importance,
lets study it.
Recursion is a function/procedure/method that calls itself again with a
smaller range of arguments (break a problem into simpler problems) or with
different arguments (it is useless if you use recursion with the same arguments).
It keeps calling itself until something that is so simple / simpler than
before (which we called base case), that can be solved easily or until an
exit condition occurs.
A recursion must stop (actually, all program must terminate). In order to
do so, a valid base case or end-condition must be reached. Improper coding
will leads to stack overflow error.
You can determine whether a function recursive or not by looking at the
source code. If in a function or procedure, it calls itself again, it's
recursive type.
Why recursive calls is possible?
When a method/function/procedure is called:
– caller is suspended,
– "state" of caller saved,
– new space allocated for variables of new method.
With recursive call, same things happen.
Recursion is a powerful and elegant technique that can be used to solve
a problem by solving a smaller problem of the same type. Many problems in
Computer Science involves recursion and some of them are naturally recursive.
If one problem can be solved in both way (recursive or iterative), then
choosing iterative version is a good idea since it is faster and doesn't
consume a lot of memory. Examples: Factorial, Fibonacci, etc.
However, there are also problems that are can only be solved in recursive
way or more efficient in recursive type or when it's iterative solutions
are difficult to conceptualize. Examples: Tower of Hanoi, Searching (DFS,
BFS), etc.
Types of recursion
There are 2 types of recursion, Linear recursion and multiple branch
(Tree) recursion.
1. Linear recursion is a recursion whose order of growth is linear (not
branched).
Example of Linear recursion is Factorial, defined by fac (n)=n * fac (n-1).
2.
Tree recursion will branch to more than one node each step, growing up very
quickly. It provides us a flexible ways to solve some logically difficult
problem. It can be used to perform a Complete Search (To make the computer
to do the trial and error). This recursion type has a quadratic or cubic
or more order of growth and therefore, it is not suitable for solving "big"
problems. It's limited to small. Examples: solving Tower of Hanoi, Searching
(DFS, BFS), Fibonacci number, etc.
Some compilers can make some type of recursion to be iterative. One example
is tail-recursion elimination. Tail-recursive is a type of recursive which
the recursive call is the last command in that function/procedure. This
Tips on Recursion:
1. Recursion is very similar to mathematical induction.
2. You first see how you can solve the base case, for n=0 or for n=1.
3.
Then you assume that you know how to solve the problem of size n-1, and
you look for a way of obtaining the solution for the problem size n from
the solution of size n-1.
When constructing a recursive solution, keep the following questions in
mind:
1. How can you define the problem in term of a smaller problem of the same
type?
2. How does each recursive call diminish the size of the problem?
3.
What instance of the problem can serve as the base case?
4.
As the problem size diminishes, will you reach this base case?
Sample of a very standard recursion, Factorial (in Java):
static int factorial(int n) {
if (n==0)
return 1;
else
return n*factorial(n-1);
}
References:
-. CLRS chapter 4, Recurrences
4.
Divide and Conquer
Whatever man prays for, he prays for a miracle.
Every prayer reduces itself to this
"Great God, grant that twice twos be not four"
(read as: two 2s or 2+2 != 4)
[DAA chapter 4]
This technique use analogy "smaller is simpler". Divide and conquer algorithms
try to make problems simpler by dividing it to sub problems that can be
solved easier than the main problem and then later it combine the results
to produce a complete solution for the main problem.
Divide and Conquer algorithms: Quick Sort, Merge Sort, Binary Search.
Believe it or not, it is a general principle
of designing algorithms. If you want to solve something, try thinking in
Divide & Conquer way first.
Divide & Conquer has 3 main steps:
1. Divide data into parts
2. Find sub-solutions for each of the parts recursively (usually)
3. Construct the final answer for the original problem from the sub-solutions
In pseudo code, Divide and Conquer can be
written like this:
DC(P) {
if small(P) then
return Solve(P);
else {
divide P into smaller instances P1,...,Pk, k>1;
Apply DC to each of these subproblems;
return combine(DC(P1),...,DC(Pk));
}
}
Application of Divide & Conquer: Binary Search
Binary Search,
is not actually follow the pattern of the full version of Divide & Conquer,
since it never combine the sub problems solution anyway. However, lets use
this example to illustrate the capability of this technique.
Binary Search
will help you find an item in an array. The naive version is to scan through
the array one by one (sequential search). Stopping when we found the item
(success) or when we reach the end of array (failure). This is the simplest
and the most inefficient way to find an element in an array. However, you
will usually end up using this method because not every array is ordered,
you need sorting algorithm to sort them first.
Binary search
algorithm is using Divide and Conquer approach to reduce search space and
stop when it found the item or when search space is empty.
Pre-requisite
to use binary search is the array must be an ordered array, the most commonly
used example of ordered array for binary search is searching an item
in a dictionary.
Efficiency
of binary search is O(log n). This algorithm was named "binary" because
it's behavior is to divide search space into 2 (binary) smaller parts.
Complete binary
search algorithm:
1. Let
a be an ordered array
Let i be the start index
Let j be the ending index
Let aWord be the word we are looking for
2. Do a recursive binary search, starting with i=1
and j=total elements in the array.
3. If i>j and we haven't found aWord, stop search, announce failure
4. If a[(i+j) div 2] equal to aWord then
stop search, announce success
else if a[(i+j) div 2] smaller than aWord then
call binary search with i unchanged and j=(i+j) div
2
else if a[(i+j) div 2] greater than aWord then
call binary search with i=(i+j) div 2 and j unchanged
References:
-. DAA chapter 4
-. Any other algorithm books
5.
Backtracking plus Branch and
Bound
Backtracking is a recursive solution for combinatorial
objects (Depth First Search). While Branch and Bound is a more sophisticated
version of backtracking, done in Breadth First Search manner, and use heuristic
functions to quickly prune unpromising search region.
Please read other references book regarding
these two useful techniques.
6.
Optimizing your source code
Your code must be fast. If it cannot
be "fast", then it must at least "fast enough".
If time limit
is 1 minute and your currently program run in 1 minute 10 seconds, you may
want to tweak here and there rather than overhaul the whole code again...
There are
many tricks that you can use to optimize your code. Understanding computer
hardware, especially I/O, memory, and cache behavior, can help you design
a better program. Please do a Google search to find information regarding
how to speed up your code using your preferred programming language.
Note: Redesigning your algorithm
in a better way (like choosing an algorithm with better worst case order
of growth, or choosing a faster data structures) always outperform
any compiler-optimization trick. Every program is doing the most task in
only about 10% of the code. Find this "critical code" and find a way to
optimize it.
7.
Some solution paradigms
Here is some things that you
need to consider when designing your solution :)
Generating
vs Filtering
Programs that
generate lots of possible answers and then choose the ones that are correct
(imagine an 8-queen solver) are filters. Those that hone in exactly on the
correct answer without any false starts are generators. Generally, filters
are easier (faster) to code and run slower. Do the math to see if a filter
is good enough or if you need to try and create a generator.
Pre-Computation
/ Pre-Calculation
Sometimes
it is helpful to generate tables or other data structures that enable the
fastest possible lookup of a result. This is called Pre-Computation (in
which one trades space for time). One might either compile Pre-Computed
data into a program, calculate it when the program starts, or just remember
results as you compute them. A program that must translate letters from
upper to lower case when they are in upper case can do a very fast table
lookup that requires no conditionals, for example. Contest problems often
use prime numbers - many times it is practical to generate a long list of
primes for use elsewhere in a program.
Decomposition
(The hardest thing at programming contests)
While there
are fewer than 20 basic algorithms used in contest problems, the challenge
of combination problems that require a combination of two algorithms for
solution is daunting. Try to separate the cues from different parts of the
problem so that you can combine one algorithm with a loop or with another
algorithm to solve different parts of the problem independently. Note that
sometimes you can use the same algorithm twice on different (independent!)
parts of your data to significantly improve your running time.
Symmetries
Many problems
have symmetries (e.g., distance between a pair of points is often the same
either way you traverse the points). Symmetries can be 2-way, 4-way, 8-way,
and more. Try to exploit symmetries to reduce execution time.
For instance,
with 4-way symmetry, you solve only one fourth of the problem and then write
down the four solutions that share symmetry with the single answer (look
out for self-symmetric solutions which should only be output once or twice,
of course).
Solving forward
vs backward
Surprisingly,
many contest problems work far better when solved backwards than when using
a frontal attack. Be on the lookout for processing data in reverse order
or building an attack that looks at the data in some order or fashion other
than the obvious.
Simplification
or in technical terms: Problem Reduction
Some problems
can be rephrased into a somewhat different problem such that if you solve
the new problem, you either already have or can easily find the solution
to the original one; of course, you should solve the easier of the two only.
Alternatively, like induction, for some problems one can make a small change
to the solution of a slightly smaller problem to find the full answer.
References:
-. USACO training gateway
What's next?
With these basic data structures
(chapter 2) and basic techniques (chapter 3), you are able to solve majority
of simple problems. Now you are ready to tackle an even harder problems
using more sophisticated algorithms. But before that... lets enrich your
mathematics. Computer Science and Mathematics are closely intertwined, so,
stay tuned :D
This document, 3_basic_algorithms.html, has been accessed 14261 times since 26-Jul-04 19:55:55 SGT.
This is the 10th time it has been accessed today.
A total of 9085 different hosts have accessed this document in the
last 1591 days; your host, 38.103.63.56, has accessed it 1 times.
If you're interested, complete statistics for
this document are also available, including breakdowns by top-level
domain, host name, and date.
|