Download as pdf or txt
Download as pdf or txt
You are on page 1of 15

COS351-D/202/2004

COS351-D

ARTIFICIAL INTELLIGENCE TECHNIQUES

TUTORIAL LETTER 202

DEPARTMENT OF THEORETICAL COMPUTING

SCHOOL OF COMPUTING
2

Question 1
i)
3 COS351-D/202/2004

In the case of the solution presented on the previous page we have chosen the heuristic function
¡(n) to be equal to the number of queens that still have to be placed. This heuristic is admissible,
since ¡(n) = h(n). The heuristic also causes the f-hat-values to remain similar on all levels.
Assume that the root node is on level 0. The number of queens that still needs to be placed on
level 1 are 3, hence ¡(n) = 3 and g(n) = 1, therefore f-hat = 4. The values ¡(n) = 3, g(n) = 1 and f-
hat = 4 hold for all the nodes on level 1 consequently we have only indicated the respective
values once for all the nodes on level1.
We find that all the ¡(n), g(n) and f-hat values are equal for nodes on the same level therefore
these values are only described once for each level.

ii) We discuss three different heuristics:

a. Setting ¡(n) = 0 for all nodes n assures admissibility, but it also results in a
uniform-cost search.

b. A second possible heuristic is to set ¡(n) = the remaining number of queens that
have to be placed. We have used this heuristic in the first part of the question.
This heuristic is also admissible, since ¡(n) = h(n).

c. Another possible choice is a heuristic where we take into consideration the


number of remaining cells in which a queen can still legally be placed. Let q
denote the number of these remaining cells. We can set ¡(n) = q(n)/16.

iii) Is the heuristic discussed in ii.c admissible? Well, the real cost of a node n to the goal
node, h(n) is equal to the length of the path from n to the goal node. (We assume that the
cost of placing a single queen is one.) We have to make sure that ¡(n) # h(n) for all nodes
n. If n is a goal node then q(n) is zero, so ¡(n) = h(n) = 0. If n is any other node then ¡(n)
# 1 # h(n). So this heuristic is admissible.

Is it necessary to use an admissible heuristic for the four-Queens problem? Well, if we


have such a heuristic, then we are assured that the A* algorithm find a shortest path to the
goal node. However, in this problem all paths to goal nodes have the same length! So we
do not need to select an admissible heuristic.

Hence we can also simply choose ¡(n)=q(n) for all n.

Question 2

Our program is implemented in C++. We discuss various aspects of the program below,
including a cost function and different choices of heuristics. Even though your choice of
heuristic and cost functions may differ from ours, the basic A* algorithm remains the same.

If you found this task a bit overwhelming, please skip the first few paragraphs below and first
4

look at our pseudocode A* algorithm. This algorithm is an adaptation of the A* algorithm on


pages 144-145. Once you understand the steps we have to perform, you can work your way into
the details.

Nodes, generating successor nodes and operators

Before we rush in and start writing programs, it is a good idea to sit back and make sure that we
know exactly what it is that we want to do. In particular, we have to make sure that we know
what a node (representing a state) and an operator (representing an action) look like.

Any specific node tells us how many queens have been placed on the board in the state it is
representing, and where they have been placed. So, we represent the appearance of the entire
board up to the placement of the last queen. For our convenience, we also store a node’s g and f^
values (and we have a member function to calculate the value of ¡.)

An operator is then merely the placement of a queen in some column where no queen has been
placed yet. For example, in the Eight-Queens problem there are 8 possible rows in which a queen
can be placed in one column: so there are 8 operators.

This brings us to the different kinds of nodes. We start with the start node, representing an
empty board. In exercise 9.1 Nilsson assumes that we can generate any successor to the start
nodes, i.e. place a queen in any position on the board. He then eliminates some successors
because they are symmetric to others. This is fine for a 4x4 board but we need a better approach
when we deal with larger sized boards. We are only going to consider successors to the start
node where we are allowed to place a queen only in the first column. So, for an nxn board, we
have n successors (we do not take symmetry into consideration). In the Eight-Queens problem
with an 8x8 board, there are eight different successors to the start node: one in which the first
queen is placed in column one row one, one in which she is placed in column one row two, and
so on, up to column one row eight. In the next level of the search graph (at depth 2), we only
consider nodes where a queen is placed somewhere in the second column. In nodes at depth 3,
we only have nodes where a queen is placed in the third column, etc.(You may want to read the
first two paragraphs of Section 11.2 in Nilsson: here they discuss the same approach to solving
the Eight-Queens problem.)

What about illegal nodes? The nodes we don’t want to occur are those representing states in
which a queen is being threatened. When we apply an operator (i.e. generate a successor node) it
amounts to placing a queen somewhere in the next empty column. It only makes sense to place
another queen if we are sure that in all the columns to the left of the next empty column, none of
the queens are being threatened. If we agree to abide by this rule right from the start, it means
that, whenever we make the next move, we only have to check whether the queen placed in the
next empty column is being threatened. Determining whether a queen is being threatened is a
tricky issue and we discuss it a bit later in detail.

Next we discuss recurring nodes. A recurring node is one that contains a state that is identical to
5 COS351-D/202/2004

one contained in one of its ancestors. Here we are lucky. The way in which we generate
successors ensures that there can be no recurring nodes. In the nodes at depth one of the search
tree, a queen is placed somewhere in the first column; in the nodes at depth two of the search
tree, a queen is placed in the second column, etc. This means that we do not need to use the
modified step 6 given on page 143 of the textbook.

This takes care of the different kinds of nodes, but it still leaves us to decide what a goal node
should look like. A goal node represents any state in which no queen is being threatened, and in
which we have a queen in every one of the columns, i.e. the node is at depth 8 of the search tree
in the Eight-Queens problem.

Now we have to decide how we are going to represent nodes and operators in our program. We
define a NodeType class to represent a node.

class NodeType {
public:
NodeType ();
void SetQueen (int row);
int GetQueen (int column) const;
int GetG () const {return g;};
double CalcF ();
bool operator<(const NodeType & right) const;
private:
double CalcH ();
vector <int> board;
int g;
double f;
};

The board can be represented by a one-dimensional array, and we use a vector. (Note that
using a two-dimensional array is space inefficient.) To represent a queen placed in column i, we
simply record the row number in which the queen is placed in entry i. Consider the solution to
the Eight-Queens problem in figure 11.1 on page 182 in Nilsson. Assume that the columns and
the rows are numbered from 0 to 7. The vector board representing this solution is as follows:
board[0]=0 board[1]=6 board[2]=4 board[3]=7
board[4]=1 board[5]=3 board[6]=5 board[7]=2

SetQueen is a mutator that sets a queen in row number row in the next available column.
GetQueen is an inspector that returns the row number in which the queen in column number
column has been placed. GetG is an inspector that returns the g value of the node. The g data
member value is equal to the number of queens that have been placed. The data member f is
actually f^. The value of ¡ is calculated by the private data member CalcH - it is not necessary to
store this value explicitly.

The operators are represented implicitly. You can look at the code to see this.
6

The algorithm

We use the A* algorithm on pages 144-145 with a few simplifications:


• We do not need a CLOSED list because all goal nodes have the same total costs in this
problem. This characteristic of our problem means that we do not have to redirect
pointers in step 7. In fact, it also means that we do not have to stick to a heuristic that is
admissible! After you feel that you understand the program below, please come back to
this point and make sure that you agree with it.
• In step 6 we do not need to check for successors that are ancestors of node n because this
is impossible in our case.

Here is a pseudocode version of the algorithm:

generate the start node and place on list OPEN


while OPEN is not empty do
{
remove a node from OPEN and call it n
if n is a goal node
{
print the solution
remove all nodes from OPEN
}
else
{
expand node n and place all legal successors on OPEN
reord
er
nodes
on
OPEN
in
incre
asing
order
accor
ding
to
their
^
f
value
s
}
}

We have already discussed the start node and the goal node.

The best choice for the OPEN list is to use a queue data structure. In C++, however, the STL
list container is very useful because we can remove a node from the front and easily sort the
7 COS351-D/202/2004

list.

It is very easy to expand a node (i.e. to generate all its successors), but a bit more tricky to check
whether a successor is a legal node. Let us consider a node (in the Eight-Queens problem) where
we have already placed four queens which we want to expand. The eight successors are simply
all the nodes where we have to place a queen in column number 4. Note that we number the
columns and rows from 0 to 7. A successor is legal if it does not threaten any of the four queens
that have already been placed.

How do we determine whether a queen in the next empty column is being threatened? Let’s look
at a concrete example. Consider the following 8×8 board:

Q
Q
Q
Q
Q

How do we check whether the queen in column number 4 is threatened? Firstly, we need only
look at columns to the left of column number 4. Secondly, of all those cells in the columns to the
left of column number 4, we only need to check whether there is a queen in one of the shaded
cells. That is, since the queen in column number 4 is in row number 3, we need to look in the
following cells:
C Row number 3 in column numbers 3, 2, 1 and 0. This corresponds to the horizontal group
of shaded cells.
C Row number 2 of column number 3, row number 1 of column number 2 and row number
0 of column number 1. This corresponds to the group of shaded cells on the diagonal
going left and up.
C Row number 4 of column number 3, row number 5 of column number 2, row number 6 of
column number 1 and row number 7 of column number 0. This corresponds to the group
of shaded cells on the diagonal going left and down.

Can you see a pattern in the last two groups of cells? Every time we move one column to the left,
we move one row up in the one group, and one row down in the other group. So in the
RenegadeNode function we check whether these groups of cells contain at least one queen. If
one of them does, the queen in the last column is threatened, otherwise she isn’t.
8

The cost function

The total cost of a node n from the start node, g(n), is simply the depth of the node n. So the goal
node in an N-Queens problem has a cost of N.

The heuristic function

An easy choice is to set h(n) = 0 for all nodes n. It is a safe choice because it is admissible, but
not always the most effective because it results in a uniform cost search.

We opted to use h(n) = q(n) where q(n) is equal to the number of unthreatened positions on the
board. This heuristic is not admissible, but it does not matter. Keep in mind that an admissible
heuristic ensures that A* always generates the cheapest solution. However, in our case all the
goal nodes have a cost equal to the size of the problem, so one goal node cannot be a better
solution than another one.

In our program we use an nxn sized array to calculate the value of q. We first initialise every
entry to zero and then we set all entries corresponding to threatened spaces equal to one. Finally
we count the number of entries that still have zero values. Please note that we ignore the columns
where queens have already been placed because we know that all the entries corresponding to
those columns are threatened.

This seems very inefficient. A better approach may be to count the number of threatened spaces
while you are generating the various diagonals, rows and columns, and subtract the total from
the number of available spaces. The problem with this approach is that it is very hard to ensure
that you do not count a threatened space more than once. Keep in mind that a space is often
threatened by more than one queen.

You can make h(n) admissible by adjusting it to h(n) = q(n)/(size*size). This affects the
efficiency of the search negatively.

The code

There are three files: the main program, a header file called QueensDec.h and its
implementation in the file called QueensDec.cpp.

The header file and its implementation file:

The declaration of the class NodeType appears in the header file. The size of the problem is
given by the value of the constant MAX.

QueensDec.h
9 COS351-D/202/2004

#include <vector>
#include <iostream>

const int Max=8; // Size of the board

class NodeType {
public:
NodeType();
void SetQueen(int row);
int GetQueen(int column) const;
int GetG () const {return g;};
int CalcF ();
bool LegalNode(int row);
bool operator<(const NodeType & right) const;
private:
int CalcH ();
vector <int> board;
int g; // the numbers of queens that have been placed
int f;
};

QueensDec.cpp

#include "QueensDec.h"

NodeType::NodeType() {
board.resize(Max);
f=-1; // Do not have to initialise this value
g=0;
}

// Place a new queen in the next available column and the given row
number
void NodeType::SetQueen (int row) {
board[g++]=row;
}

// Returns the row in which a queen has been placed in the given
column
int NodeType::GetQueen(int column) const {
return (board[column]);
}

// Counts the remaining number of unthreatened spaces on the board


int NodeType::CalcH () {
int q=0;
int row,newRow,newCol;
int spaces[Max][Max];
// use the array spaces to set threatened spaces equal to 1
// init spaces array to zero except for columns with placed
queens
for (int i=0; i<Max; i++)
10

for (int j=g; j<Max; j++)


spaces[i][j] = 0;

for (int i=0; i<g;i++) { //for every placed column


row=board[i];
// set entries corresponding to row of a placed queen in
// col i to 1
for (int j=g; j<Max; j++)
spaces[row][j] = 1;
// set diagonal entries of placed queen in col i to 1
newRow = row + (g-i);
newCol = g;
while ((newRow < Max) && (newCol < Max)) {
spaces[newRow][newCol] = 1;
newRow++;
newCol++;
}
newRow = row - (g-i);
newCol = g;
while ((newRow >= 0) && (newCol < Max)) {
spaces[newRow][newCol] = 1;
newRow--;
newCol++;
}
}
for (int i=0; i<Max; i++)
for (int j=g; j<Max; j++)
if (!spaces[i][j]) q++;
return q;
}

int NodeType::CalcF () {
int h;

h=CalcH();
f = g + h;
return f;
}

// Checks if a Queen in the new column and row threatens a


// previously placed queen
bool NodeType::LegalNode(int row) {
bool legal=true;
int i=1;
int queenPlaced;

if (g > 0) { //Only relevant if at least one queen has been


placed
while (legal && i<=g) {
queenPlaced = board[g-i];
if ((queenPlaced == row - i) ||
(queenPlaced == row + i) ||
11 COS351-D/202/2004

(queenPlaced == row))
legal = false;
i++;
}
}
return legal;
}

// overload the < operator to enable the STL sort algorithm


bool NodeType::operator< (const NodeType &right) const {
if (f < right.f)
return true;
else
return false;
}

The main program:

#include <iostream>
#include <list>
#include "QueensDec.h"

void GenStartNode (NodeType & n, list <NodeType> & OPEN);


void GetNode (NodeType & n,list <NodeType> & OPEN);
void ExpandNode (NodeType n,list <NodeType> & OPEN);
void SortOPEN (list <NodeType> & OPEN);
bool IsGoalNode(NodeType n);
bool LegalNode(NodeType n, int row);
void ClearOPEN(list <NodeType> & OPEN);
void PrintSolution (const NodeType & n);

int main() {
list <NodeType> OPEN;
NodeType n;

GenStartNode(n,OPEN);
while (! OPEN.empty()) {
GetNode(n,OPEN);
if (IsGoalNode(n)) {
PrintSolution(n);
ClearOPEN(OPEN);
}
else {
ExpandNode(n,OPEN);
SortOPEN(OPEN);
}
}
return 0;
}

void GenStartNode (NodeType & n, list <NodeType> & OPEN) {


12

OPEN.push_back(n);
}

void GetNode (NodeType & n,list <NodeType> & OPEN) {


n = OPEN.front();
OPEN.pop_front();
}

// Generate successors and put on OPEN


void ExpandNode (NodeType n,list <NodeType> & OPEN) {
NodeType succ;
int num;
for (int move=0; move<Max; move++) {
if (n.LegalNode(move)) {
succ=n;
succ.SetQueen(move);
succ.CalcF();
OPEN.push_back(succ);
}
}
}

void SortOPEN (list <NodeType> & OPEN) {


OPEN.sort();
}

bool IsGoalNode(NodeType n) {
int g;

g=n.GetG();
if (g == Max)
return true;
else
return false;
}

void ClearOPEN(list <NodeType> & OPEN) {


OPEN.clear();
}

void PrintSolution (const NodeType & n) {


int row;
cout << "The solution" << endl;
cout << "------------" << endl;
cout << endl;
for (int i=0; i<Max; i++) {
row = n.GetQueen(i);
cout << "column " << i << " = " << row << endl;
}
}
13 COS351-D/202/2004

The Solutions

The first print-out is the solution to the Eight-Queens problem and the second print-out is the
solution to the Four-Queens problem.

The solution
------------

column 0 = 0
column 1 = 4
column 2 = 7
column 3 = 5
column 4 = 2
column 5 = 6
column 6 = 1
column 7 = 3

The solution
------------

column 0 = 1
column 1 = 3
column 2 = 0
column 3 = 2

Question 3 Exercise 9.3 on page 161

In the A* algorithm we place the successors of an expanded node on OPEN but we do not check
to see whether a successor is a goal node. The question asks whether we cannot perform this
check and terminate if the successor is indeed a goal node. The problem with this suggestion is
that we cannot be sure that A* has already found the shortest path to a goal node on OPEN.
However, when a node is selected for expansion, we know that the A* algorithm has found the
shortest path to it.

Consider the example on the next page. The numbers next to the nodes are the g+¡ values, and
the numbers next to the arcs are their costs. The goal node at the bottom is placed on OPEN, but
later, when the rightmost node is expanded, a shorter path to it is found.
14

0+2

1 + 0.5

2+1 Question 4 Exercise 11.4 on


page 193
2 + 0.5
Nilsson’s HILLCLIMB (or in this
1 case hill descending) algorithm
cannot be used to solve the
problem because the f values in the
missionary and cannibals problem
do not decrease monotonically. We
Goal node will never reach a solution.

We made a modification to the hill


climbing algorithm in the study guide. Our algorithm can be used to solve the problem.

Question 5
15 COS351-D/202/2004

The minimax values for the different nodes appear underneath the nodes in the figure above.

i) The first player should choose B.

ii) If we apply the alpha-beta algorithm there occur tree cut-offs.

- A β-cut-off at node F, because 3 # 5, hence node N needs not be examined.


- A α-cut-off at node C, because 0 # 3, hence node H and its successors nodes P
and Q need not be examined.
- A α-cut-off at node D because 2 # 3, hence node J and its successors nodes T and
U need not be examined.

©
UNISA

You might also like