|
View previous topic :: View next topic |
Author |
Message |
| Lummox JR
| Joined: 07 Sep 2005 | Posts: 202 | : | | Items |
|
Posted: Wed Sep 14, 2005 7:26 pm Post subject: Subset algorithm with bitwise math |
|
|
I wrote this algorithm to solve for subsets via bitwise arithmetic. I hope people will find it useful.
Code: | static int countbits(int b)
{
int n=0;
while(b) {b&=b-1; ++n;}
return n;
}
int puzzle::TestSubset(int limit,int *flags,int *x,int *y)
{
int i,j,k,c,C,b,mb,nb,ret=0,mi;
int *result=new int[size*4];
int *stack=&result[size*2];
int *bits=&result[size*3];
memset(result,0,sizeof(int)*size*2);
if(!limit) limit=size/2;
// test by 1st part of flags array
for(i=C=mb=0;i<size;++i) if(flags[i]) {stack[C++]=mi=i; mb|=flags[i];}
if(C<4) goto cleanup;
k=-1; b=c=0;
while(stack[0]<mi) {
j=C;
if(k<mi && c<limit && c*2+2<=C) {
for(i=c;i<C;++i) {if(stack[i]>k && (j>=C || stack[i]<stack[j])) j=i;}
}
if(j>=C) {
if(!c) break;
k=stack[--c];
b=bits[c];
continue;
}
k=stack[j];
stack[j]=stack[c];
stack[c]=k;
bits[c++]=b;
nb=countbits(b|=flags[k]);
if(nb<=c) {
for(i=c;i<C;++i) {
j=stack[i];
if((result[j]=flags[j]&b)) {++ret; flags[j]&=~b;}
}
if(ret) {*x=ints2bits(stack,c); *y=b; ret=1; goto cleanup;}
k=mi; continue;
}
else if(nb>limit || nb*2>C) {k=mi; continue;}
}
// test by 2nd part of flags array
for(i=C=mb=0;i<size;++i) if(flags[i+size]) {stack[C++]=mi=i+size; mb|=flags[i+size];}
k=size-1; b=c=0;
while(stack[0]<mi) {
j=C;
if(k<mi && c<limit && c*2+2<=C) {
for(i=c;i<C;++i) {if(stack[i]>k && (j>=C || stack[i]<stack[j])) j=i;}
}
if(j>=C) {
if(!c) break;
k=stack[--c];
b=bits[c];
continue;
}
k=stack[j];
stack[j]=stack[c];
stack[c]=k;
bits[c++]=b;
nb=countbits(b|=flags[k]);
if(nb<=c) {
for(i=c;i<C;++i) {
j=stack[i];
if((result[j]=flags[j]&b)) {++ret; flags[j]&=~b;}
}
if(ret) {*y=ints2bits(stack,c,-size); *x=b; ret=2; goto cleanup;}
k=mi; continue;
}
else if(nb>limit || nb*2>C) {k=mi; continue;}
}
cleanup:
if(ret) memcpy(flags,result,sizeof(int)*size*2);
delete result;
return ret;
} |
Here's how it works. When testing a house for naked and hidden subsets, you need to load up the flags array. The array is set up so that the first 9 values (assuming size=9) will test vertically, and the next 9 horizontally. That is, if you were to view the array as a binary matrix, the right half would be the x/y transpose of the left half. While it's possible for TestSubset() to build the right half on its own, it would probably take longer. I'll refer to position in each half of the array as "x" and the bit as "y". For hidden subsets, for example, digit would be x and position within the house would be y; naked subsets would be the opposite.
TestSubset() then tests each half of the array. It assumes the size of a subset can only be as big as limit, or size/2, whichever is smaller; if a subset exists larger than size/2 in the left half, there will be a corresponding smaller subset in the right half. The limit argument is included so that a human-style solver can restrict itself to pairs/X-wings first, then move on to triples/swordfish and so on. While keeping the "x" values in the subset in ascending order for easy recursion, it will find the next untested x value and see if that can be a part of the subset. If no x value is available or the subset is already at the limit, it pops the last value off the subset stack. Once the number of bits, ORred together for each item the subset, is equal to the size of the subset, a valid subset has been found. It is then tested to find out if this is actually new information. If so, the result array is loaded with bit flags representing candidates that should be eliminated (all other x values not in the subset, for all y values in the subset). Then the result array is copied over the flags array so the calling function can make eliminations, and the return value is 1 for a subset found in the left half, 2 in the right half. The values pointed to by x and y are loaded with the x and y values of the subset, converted to bit flags. (If on the left half x is digit and y is position, then *x will be the digits and *y will be the positions no matter which type of subset was found. These variables are used for handy reference if you're printing out a hint.)
Setting up the flags array is fairly simple and looks something like this:
Code: | // go through the house columns in a DLX matrix
for(c=sizesq;c<sizesq*4;c+=size) {
memset(flags,0,sizeof(int)*size*2);
for(i=0;i<size;++i) {
if(marked_col[c+i]) continue;
for(j=0;j<size;++j) {
if(!marked_row[row[c+i][j]]) {
flags[i]|=(1<<j);
flags[j+size]|=(1<<i);
}
}
}
} |
This will put hidden subsets in the left half, and naked subsets on the right. Once you get a return value from TestSubsets(), you can eliminate values like so:
Code: | for(i=0;i<size;++i)
for(j=0;j<size;++j)
if((flags[i]&(1<<j)) || (flags[j+size]&(1<<i)))
++marked_row[row[c+i][j]]; |
The subset solver will also find X-wings and swordfish, since those are just positional versions of the subset rule. For each digit, you merely have to load it with bit flags based on possible x/y positions of that digit in the sudoku grid. This too is very easy with a dancing links matrix.
Code: | for(N=0;N<size;++N) {
memset(flags,0,sizeof(int)*size*2);
for(i=0,r=N;i<size;++i) {
for(j=0;j<size;++j,r+=size) {
if(!marked_row[r]) {
flags[i]|=(1<<j);
flags[j+size]|=(1<<i);
}
}
}
} |
|
|
Back to top |
|
|
| SarC
| Joined: 19 Oct 2005 | Posts: 14 | : | | Items |
|
Posted: Wed Oct 26, 2005 11:00 am Post subject: |
|
|
I want, like most other here, program my own logic sudoku-solver. And after twisting my brain trying to find a good algorithm to find naked and hidden pairs/triples etc, I found this post.
But my problem is that I've never programmed in C++ so I can't really read the source. I've read through your explanation a couple of times, but I can't really figure it out.
So if anyone could translate it to Delphi/Pascal that would be amazing, but that's asking too much heh. But Lummox JR, if you could show me an example of how your routine works, I might be able to figure out the logics, and program my own copy in Delphi. |
|
Back to top |
|
|
| Lummox JR
| Joined: 07 Sep 2005 | Posts: 202 | : | | Items |
|
Posted: Wed Oct 26, 2005 8:52 pm Post subject: |
|
|
Well, it is kind of a complicated algorithm, so no surprise that it was hard to figure out.
The most important thing to start with is that you must have an array of size*2, i.e. 18 for standard sudoku. The first half should be loaded with bit flags for one orientation, and the second half should have them transposed. For naked subsets, each "column" (element in the array) is a position within a box/column/row; each "row" (bit) is a digit. Hidden subsets do this the opposite way. This algorithm relies on both being present, so it can bail out of one test and try another to save time. If you have only 7 choices to loop through, the most you'll find is a triple, so if a naked pair or triple can't be found, it will try finding hidden pairs or triples.
Now the simple parts. Clear out the result array, which is also size 18, for use later. For each half of the flags array, do the following:
1) Count the number of choices, C. Any array element or bit set to 0 represents a constraint that was already filled, so we don't worry about it. The number of choices is the number of non-zero values in this half of the flags array. As you count the choices, also put them into the stack array and also find the "highest" choice, mi. If C<4, we can't possibly find any subset as large as a pair, so bail out.
2) The stack array works as follows: It holds the current subset at the beginning, the other choices at the end. You also keep track of the current subset size, c, and the last value you added or tried to add to the subset, k. The subset is stored in ascending order. There is also a bits stack array, bits, which we will fill later. The bit values of the current subset are stored in b. Start with k=-1 (or size-1 for the 2nd half of the flags array), c=0, and b=0.
3) Set j=C. If we are not past the limit (limit=2 is a pair), and if (c+1)<=C (i.e., we're not looking for too big a subset), and if k<mi (there are still more choices for the subset in ascending order), find the next possible value stack[j] to add to the subset. Do this by looping through i from c to C-1, and if j>=C (no j chosen yet) or stack[i]<stack[j] (smaller value found), set j=i.
4) If j>=C, that is if nothing was found, pop k from the stack. If there's nothing to pop, move on to the next half of the flags array or else exit the function with return value 0 (no subsets found). Otherwise, decrement c, and then pull b=bits[c] off the stack.
5) Swap stack[j] and stack[c] so the first part of stack, the part that holds the subset we're looking at, is in ascending order. Set k=stack[c], push k to the stack, and push b to the bits stack. Now add the bits of flags[k] to b via OR, and find nb (number of bits) with countbits(). I'll explain that function later.
6) If nb<=c, we have a subset size c with nb values; they should be equal if so; the <= test is just for safety. If nb>c instead, go to step 7. If we have a subset, we want to eliminate the bits in b from all other parts of this half of the array that are not in the set. Now, search all remaining values in the stack array that are not part of the current subset (indexes c through C-1). If for any i from c to C-1, you find that flags[stack[i]] AND b is nonzero, set result[stack[i]]=flags[stack[i]] AND b, and also note that we will be returning a positive value. If a result was not found, set k=mi to stop looking for additonal columns for this subset; it's not one that interests us. If a result was found:
6a) If we are in the first half of the flags array, set ret=1 (the return value), and *x=ints2bits(stack,c) and *y=b. ints2bits fills in bit flags with the columns that were part of the subset, stack[0] through stack[c-1]. Go to the cleanup section to deallocate memory and also copy the result array into the flags array.
6b) If we are in the second half of the flags array, set ret=2, *y=ints2bits(stack,c), and *x=b. Go to the cleanup section.
7) If nb>=c, then if nb>limit, we have too many values to make a set within our limit. If nb*2>C, then if there is a subset to be found we're better off searching the other half of the flags array. Either way, set k=mi to stop searching within this potential subset; it does not interest us.
Now once this routine returns, if ret==1 it found a subset in the first half of the flags array; otherwise if ret==2 it found a subset in the second half. This tells you the difference between naked and hidden subsets, or X-wings/swordfish by column or row. If you set this up so the first half is for naked subsets, the x value now has the positions for the subset, and y has the digits, both stored in bit flag form. The flags array will have all 0's except in places where you can make eliminations.
The countbits() function looks like this:
Code: | int countbits(int b)
{
int n;
for(n=0; b; ++n) b &= b-1;
return n;
} |
And ints2bits() is like so:
Code: | int ints2bits(int *A, int n)
{
int i,b;
for(b=i=0; i<n; ++i) b |= A[i];
return b;
} |
I know a lot of the above may not have shed much light on the issue, but hopefully some of the stickier parts now make sense. |
|
Back to top |
|
|
| SarC
| Joined: 19 Oct 2005 | Posts: 14 | : | | Items |
|
Posted: Wed Oct 26, 2005 11:55 pm Post subject: |
|
|
Amazing Lummox JR! Thanks alot.
I'm still trying to grasp the idea of having bits represent the values, so if you could tell me if I got this example right:
Code: |
Row: {5} {7} {1} {2,4} {2,4,9} {2,9} {3,4,8} {6} {2,3,8}
Matrix:
123456789 (value)
1
2
3
4 x x
5 x x x
6 x x
7 xx x
8
9 xx x
(row)
First part of the flag array:
0, 0, 0, 010100000, 010100001, 010000001, 001100010, 0, 011000010,
Transposed matrix:
123456789
1
2 xxx x
3 x x
4 xx x
5
6
7
8 x x
9 xx
Second part of the flag array:
0, 000111001, 000000101, 000110100, 0, 0, 0, 000000101, 000011000
|
And another thing, when you call ints2bits for the second time, you supply a third parametre:
Code: |
if(ret) {*y=ints2bits(stack,c,-size); *x=b; ret=2; goto cleanup;}
|
and I only see two parametres in the code you showed me.
Thanks again for taking your time helping me out
SarC |
|
Back to top |
|
|
| xyzzy
| Joined: 24 Aug 2005 | Posts: 80 | : | | Items |
|
Posted: Thu Oct 27, 2005 7:55 am Post subject: |
|
|
Here is a recursive algorithm which I think is much easier to understand.
Code: |
int bitsets[9];
int maxsize;
int foundset;
int search(int start, int size, int union)
{
int set;
if(size > maxsize) return 0;
for(;start<9;start++) {
if(!bitsets[start]) continue;
set = union | bitsets[start];
if(numbits(set) == size) {
/* subset found */
foundset = set;
return 1<<start;
} else {
/* search for subsets containing union made so far */
if(set=search(start+1, size+1, set)) {
/* recursive search found subset */
return (1<<start) | set;
}
}
}
return 0;
}
/* how to call */
bitsets = the nine sets to search;
maxsize = maximum subset size to search for;
sets = search(0, 1, 0);
if(sets) {
printf("the sets %x make a subset with union %x\n", sets, foundset);
} else {
printf("No subset found\n");
}
|
Last edited by xyzzy on Fri Oct 28, 2005 12:28 am; edited 2 times in total |
|
Back to top |
|
|
| SarC
| Joined: 19 Oct 2005 | Posts: 14 | : | | Items |
|
Posted: Thu Oct 27, 2005 10:43 am Post subject: |
|
|
@xyzzy: Thanks alot for helping out. I've translated your function to Delphi and tried it out, but I must be doing something wrong. With the example I posted, the function spits out 'The sets 128 make a subset with union 258', which doesn't seem to be right.
I suspect that its the way I make the bitset, so if you could check this code and see if its correct:
(example is Row: {5} {7} {1} {2,4} {2,4,9} {2,9} {3,4,8} {6} {2,3,8})
Code: |
First I made a helper function just to be sure:
funnction MakeBits(a1, a2, a3, a4, a5, a6, a7, a8, a9: Integer): Integer;
begin
Result := a1 Or (a2 shl 1) Or (a3 shl 2) Or (a4 shl 3) Or
(a5 shl 4) Or (a6 shl 5) Or (a7 shl 6) Or
(a8 shl 7) Or (a9 shl 8);
end;
Then I build the bitsets array:
Bitsets[0] := 0;
Bitsets[1] := 0;
Bitsets[2] := 0;
Bitsets[3] := MakeBits(0,1,0,1,0,0,0,0,0);
Bitsets[4] := MakeBits(0,1,0,1,0,0,0,0,1);
Bitsets[5] := MakeBits(0,1,0,0,0,0,0,0,1);
Bitsets[6] := MakeBits(0,0,1,1,0,0,0,1,0);
Bitsets[7] := 0;
Bitsets[8] := MakeBits(0,1,1,0,0,0,0,1,0);
And set MaxSize to 3.
Maxsize := 3;
And run the function
Sets := Search(0, 1, 0);
And here is the translated function:
function Search(Start, Size, Union: Integer): Integer;
Var TmpSet : Integer;
Begin
If Size > Maxsize Then
Begin
Result := 0;
Exit;
End;
While Start < 9 Do
Begin
TmpSet := Union Or Bitsets[Start];
If CountBits(TmpSet) = Size Then
Begin
Foundset := TmpSet;
Result := 1 shl Start;
Exit;
End Else
If TmpSet = Search(Start + 1, Size + 1, TmpSet) Then
Begin
Result := (1 shl Start) Or TmpSet;
Exit;
End;
Inc(Start);
End;
Result := 0;
End;
|
I've added the 'Result := 0' at the end of the function, otherwise the function result would be undefined in Delphi; I assumed the in C that the function would return 0, if not specified by a 'return'. That might be were it fails, if that's not the case. If I exclude the line, I get a 'No subset found'.
SarC |
|
Back to top |
|
|
| xyzzy
| Joined: 24 Aug 2005 | Posts: 80 | : | | Items |
|
Posted: Fri Oct 28, 2005 12:27 am Post subject: |
|
|
SarC wrote: | @xyzzy: I suspect that its the way I make the bitset, so if you could check this code and see if its correct:
|
It's the way the single sets have been changed to zero. I was trying to make my example as simple as possible so I took out that optimization. At the start of the loop you want to check if bitsets[start] == 0 and then
skip that set if it is. I updated my code to fix that, and add the missing return 0 at the end. |
|
Back to top |
|
|
| SarC
| Joined: 19 Oct 2005 | Posts: 14 | : | | Items |
|
Posted: Sun Oct 30, 2005 3:22 pm Post subject: |
|
|
xyzzy wrote: | SarC wrote: | @xyzzy: I suspect that its the way I make the bitset, so if you could check this code and see if its correct:
|
It's the way the single sets have been changed to zero. I was trying to make my example as simple as possible so I took out that optimization. At the start of the loop you want to check if bitsets[start] == 0 and then
skip that set if it is. I updated my code to fix that, and add the missing return 0 at the end. |
I've updated my code but Search() returns 0, so something isn't working. The variable foundset is set to 266 (value 2,4 and 9). Must be Delphi that screws something up, as I think the translation of the code is correct. |
|
Back to top |
|
|
| xyzzy
| Joined: 24 Aug 2005 | Posts: 80 | : | | Items |
|
Posted: Tue Nov 01, 2005 9:24 am Post subject: |
|
|
foundset is the correct value so it must be finding the subset, but just not returning the value correctly.
I've never used Delphi, but I think this line has a bug:
If TmpSet = Search(Start + 1, Size + 1, TmpSet) Then
You want to assign the result of the Search() to TmpSet, and check if it's true. You probably want to use := instead of =. Maybe you're not allowed to use a := as an expression in Delphi like you can in C? I don't think you can in Pascal. |
|
Back to top |
|
|
| SarC
| Joined: 19 Oct 2005 | Posts: 14 | : | | Items |
|
Posted: Tue Nov 01, 2005 1:42 pm Post subject: |
|
|
You are absolutely correct! I didn't notice that it wasn't a ==. C++ can be so tricky Thanks alot for looking through it and helping me out! |
|
Back to top |
|
|
| xyzzy
| Joined: 24 Aug 2005 | Posts: 80 | : | | Items |
|
Posted: Wed Nov 02, 2005 10:00 am Post subject: |
|
|
Another optimization you can do, which makes the search a great deal faster, is to make this change:
Code: |
set = union | bitsets[start];
change to:
set = union | bitsets[start];
if(numbits(set) > maxsize) continue;
|
In other words, if the union of the set already has more than maxsize bits, then there is no point in considering it further. It can only gain bits as more sets are unioned in, and so will never make a union that has maxsize or fewer bits. |
|
Back to top |
|
|
| dukuso
| Joined: 14 Jul 2005 | Posts: 424 | : | Location: germany | Items |
|
Posted: Wed Nov 02, 2005 10:55 am Post subject: |
|
|
what about this wellknown bipartite matching algorithm ?
Is it not competitive yet for 9*9 or too complicated ? |
|
Back to top |
|
|
| xyzzy
| Joined: 24 Aug 2005 | Posts: 80 | : | | Items |
|
Posted: Wed Nov 02, 2005 1:55 pm Post subject: |
|
|
dukuso wrote: | what about this wellknown bipartite matching algorithm ?
Is it not competitive yet for 9*9 or too complicated ? |
I found a good description of bipartite matching here. I see how it's a very similar situation to the subset search problem, but not how you could make an use of the algorithm for finding a perfect matching.
I can see that to solve the sudoku, you want to find a perfect matching. But for a given row/col/box/etc there are probably many perfect matchings. The problem isn't to just find one, but to find the correct one. Eg., it's easy find numbers to place in each cell of a given row, so that no number appears twice in that row. That would be a perfect matching for the row, with the cells in the row and the numbers to be placed as the two parties to be matched. The problem is creating one of these matchings that also satisfies the alldifferent constraints for the boxes and columns intersecting the row.
Of course if a perfect matching doesn't exist, then the sudoku is unsolvable. That does tell you something, and the perfect matching algorithm presented in that link will find a perfect matching or the lack of one in O(n^3) time. The algorithm I've described for subsets will prove the existence of a perfect matching or lack of it (via Hall's theorem), without actually finding a perfect match, in worst case O(n^2) time. It's faster because it doesn't actually find the matching.
But again, finding a perfect matching is useless, unless you can also prove that it is the ONLY perfect matching. In which case, it's quite useful, but I didn't find anything about bipartite matching algorithms that were concerned with this. |
|
Back to top |
|
|
| Bob Hanson
| Joined: 05 Oct 2005 | Posts: 187 | : | Location: St. Olaf College | Items |
|
Posted: Thu Nov 03, 2005 6:12 am Post subject: |
|
|
The recursive method can also be seen at http://www.stolaf.edu/people/hansonr/sudoku/subsets.js
in JavaScript it is simply:
function analyzeX(z,type,iscanfirstindex,List,ipt,jlist,ilist,nx,ny,msg){
var maxdim=List.length/2
if(ipt>=List.length||nx>maxdim||ny>maxdim)return 0
var maxlen=List.length/2+1
//propagate to next row
analyzeX(z,type,iscanfirstindex,List,ipt+1,jlist,ilist,nx,ny,msg)
if(iseliminated)return 1
//OR in these rows and count them
var i=List[ipt] //another row/col
ilist+=Pwr2[i]
if(iscanfirstindex){
jlist=jlist | Possible["X"+type][i][z]
}else{
jlist=jlist | Possible["X"+type][z][i]
}
nx=xNum(jlist)
ny++
if(nx>maxlen)return 0
if(ny==nx && analyzeFoundX(z,type,iscanfirstindex,jlist,ilist,nx,msg))return 1
return analyzeX(z,type,iscanfirstindex,List,ipt+1,jlist,ilist,nx,ny,msg)
}
As mentioned, this just keeps checking all possible combinations until it finds that the number of nodes equals the size of the set (nx=ny). It covers X-wings, swordfish, and n-tuples hidden and naked in blocks, cells, or rows, depending upon the contents of List and the parameters "type" and "iscanfirstindex".
This routine automatically returns the smallest-size hit that results in an elimination due to the double recursion at both the beginning and the end.
Also, as a note, by checking for actual elimination [in analyzeFoundX()]-- not just the presence of the object, it can continue until either exhausting the possibilities or finding something that can advance the solution.
Works like a charm. _________________ Bob Hanson
Professor of Chemistry
St. Olaf College
Northfield, MN
http://www.stolaf.edu/people/hansonr |
|
Back to top |
|
|
| siftings
| Joined: 22 Feb 2006 | Posts: 1 | : | | Items |
|
Posted: Wed Feb 22, 2006 11:56 pm Post subject: Help converting code |
|
|
I am also trying to create a solver, but in java. I tried converting your code, but the following doesn't find any subsets. I noticed your code uses the function numbits instead of countbits, but they're the same right? Thanks...
Code: |
public class SubsetSolver {
int[] bitsets;
int maxsize;
int foundset;
public SubsetSolver() {
maxsize=3;
bitsets = new int[9];
bitsets[0] = 0;
bitsets[1] = 0;
bitsets[2] = 0;
bitsets[3] = MakeBits(0,1,0,1,0,0,0,0,0);
bitsets[4] = MakeBits(0,1,0,1,0,0,0,0,1);
bitsets[5] = MakeBits(0,1,0,0,0,0,0,0,1);
bitsets[6] = MakeBits(0,0,1,1,0,0,0,1,0);
bitsets[7] = 0;
bitsets[8] = MakeBits(0,1,1,0,0,0,0,1,0);
int sets = search(0, 1, 0);
if(sets!=0) {
System.out.println("the sets "+ sets +" make a subset with union "+ foundset);
} else {
System.out.println("No subset found\n");
}
}
int MakeBits(int a1,int a2,int a3,int a4,int a5,int a6,int a7,int a8,int a9){
int result=0;
result = a1 | (a2 << 1) | (a3 << 2) | (a4 << 3) | (a5 << 4)
| (a6 << 5) | (a7 << 6) | (a8 << 7) | (a9 << 8);
return result;
}
int search(int start, int size, int union)
{
int set;
if(size > maxsize) return 0;
for(;start<9;start++) {
if(bitsets[start]==0) continue;
set = union | bitsets[start];
if(numbits(set) > maxsize) continue;
if(numbits(set) == size) {
/* subset found */
foundset = set;
return 1<<start;
} else {
/* search for subsets containing union made so far */
if((set=search(start+1, size+1, set))!=0) {
/* recursive search found subset */
return (1<<start) | set;
}
}
}
return 0;
}
int numbits(int b)
{
int n=0;
while(b==0) {b&=b-1; ++n;}
return n;
}
public static void main(String[] args) {
SubsetSolver b = new SubsetSolver();
}
}
|
|
|
Back to top |
|
|
|
|
You cannot post new topics in this forum You cannot reply to topics in this forum You cannot edit your posts in this forum You cannot delete your posts in this forum You cannot vote in polls in this forum
|
Powered by phpBB © 2001, 2005 phpBB Group
Igloo Theme Version 1.0 :: Created By: Andrew Charron
|