Reputation:
Question - You have to find the number of ways you can reach from the intial position (1,1) to the final position (N,M) [where N is the number of rows and M is the number of columns], given that:
Note: Here the array indices are 1-indexed
Some conditions:
Sample test case:
>>> 3 3
. . .
. * .
. . .
>>> 8
There will be 8 ways:
My code:
def ways(l):
# Don't know how to proceed
Testcases=int(input())
lst=[]
for i in range(Testcases):
N,M=input().split()
N,M=int(N),int(M)
for j in range(N):
s=input().split(" ",M)
lst.append(s)
ways(lst)
I have no idea how to proceed.
Upvotes: 6
Views: 2384
Reputation: 23955
(Code here.)
A naive reccurence would seem to be the sum of all ways to get to each possible predecessor. (Clearly, a blocked cell has zero ways to arrive at, and we don't count preceding cells' ways.) Let f(i, j)
represent all the ways to get to cell M[i][j]
. Then:
f(0, 0) ->
1
f(i, 0) ->
sum(f(k, 0)) for 0 ≤ k < i
f(0, j) ->
sum(f(0, k)) for 0 ≤ k < j
f(i, j) ->
sum(f(k, j)) for 0 ≤ k < i +
sum(f(i, k)) for 0 ≤ k < j
Because of the variation caused by the blocked cells, in order to avoid the extra factor in complexity from the sum, we can use tuples (num_ways_here, cumulative_from_north, cumulative_from_east)
to keep linear complexity:
A B C
D x E
F G H
I J K
(num_ways_here, cumulative_from_north, cumulative_from_east)
A (1, 1, 1)
D (1, 2, 1)
F (2, 4, 2)
I (4, 8, 4)
G (2, 2, 4)
J (6, 8, 10)
B (1, 1, 2)
C (2, 2, 4)
E (2, 4, 2)
H (8, 12, 12)
K (22, 34, 32)
The transition for turns is that when a component is missing, it inherits num_ways_here
instead of the sum with the component (in other words num_ways_here + 0
.
22 ways to get to K:
(1)
.
. x
.
. . .
(2)
.
x
.
. . .
(3)
.
. x
. . .
(4)
.
. x
.
. .
(5)
.
x
. . .
(6)
.
x
.
. .
(7)
.
. x
. .
(8)
.
x
. .
(9)
.
. x
. .
. .
(10)
.
x
. .
. .
(11)
.
. x
. . .
.
(12)
.
x
. . .
.
(13)
.
. x
. .
.
(14)
.
x
. .
.
(15)
. . .
x .
.
.
(16)
. .
x .
.
.
(17)
. . .
x
.
.
(18)
. . .
x .
.
(19)
. .
x
.
.
(20)
. .
x .
.
(21)
. . .
x
.
(22)
. .
x
.
Upvotes: 1
Reputation: 6036
When you approach this kind of problems, you should start with looking for some patterns that could come out in a subproblem. For example, one subproblem you have is to find the number of ways you can move along aligned points. First, build a table with the number of points on the left and the number of ways to reach the last one on the right.
2 1
3 2
4 4
5 8
6 16
...
It should be pretty clear that the pattern is number_of_ways = 2^(number_of_points - 2)
. I am not a mathematician, so do not ask me why, but you can easily prove it computing all the combinations, perhaps with dynamic programming. Here is the code, or you can trust me (which I strongly suggest you not to do it)
n = 12
dp = [None] * (n + 1)
def num_of_subset(n):
if dp[n] != None:
return dp[n]
elif n == 1:
return 1
else:
ways = 0
for i in range(n):
ways += num_of_subset(n - (i + 1))
dp[n] = ways
return ways
print(num_of_subset(n))
First part is gone, now we know something that makes everything much easier: every time you add a new "point", the number of possible ways duplicates.
Second part: there are many things to take into account:
It is kind of hard to explain, so here is the code:
rows = 3
cols = 3
matrix = """
...
.*.
...
"""
matrix = [list(r) for r in matrix.split()]
print(matrix)
#(from_top_distance, from_left_distance, totways_until_here)
dp = [[(-1, -1, 0)] * cols for _ in range(rows)]
for ri, r in enumerate(matrix):
for ci, c in enumerate(r):
if c == '*':
continue
if ri == 0 and ci == 0:
dp[ri][ci] = (0, 0, 1)
continue
if (ri == 0 or dp[ri - 1][ci][0] == -1) and (ci == 0 or dp[ri][ci - 1][1] == -1):
dp[ri][ci] = (-1, -1, 0)
continue
from_top = 0 if ri == 0 else dp[ri - 1][ci][0] + 1
from_left = 0 if ci == 0 else dp[ri][ci - 1][1] + 1
top_value = 0 if ri == 0 \
else dp[ri - 1][ci][2] if from_top < 2 \
else dp[ri - 1][ci][2] * 2
left_value = 0 if ci == 0 \
else dp[ri][ci - 1][2] if from_left < 2 \
else dp[ri][ci - 1][2] * 2
dp[ri][ci] = (from_top, from_left, top_value + left_value)
print(dp)
print(dp[rows - 1][cols - 1][2])
The beauty of this code (if it is correct) is that the computational complexity is O(M*N)
.
Upvotes: 1
Reputation: 2439
First I would break the problem down into smaller problems:
right
moves to get from (0,0)
to (1,M)
down
moves to get from (0,0)
to (N,1)
(0,0)
to (N,M)
(My code is 0-indexed)
This problem can be reformulated as "all list of natural numbers that sum to N/M". This has already been extensively answered here: https://stackoverflow.com/a/4633515/8106583
With slight adjustments to allow for repetitions we get:
def subset_sum(target, result, partial=[]):
s = sum(partial)
if s == target: #check if the partial sum is equals to target
result.append(partial)
return
if s >= target:
return # if we reach the number why bother to continue
for i in range(1,target+1):
subset_sum(target, result, partial + [i])
So that we can use rightMovesAll = []; subset_sum(M-1, rightMovesAll
to get all permutations of right moves (same for down) which results for M=5
in rightMovesAll=[[1, 1, 1, 1], [1, 1, 2], [1, 2, 1], [1, 3], [2, 1, 1], [2, 2], [3, 1], [4]]
Now for each combination of one rightMoves
and one downMoves
we need to interlace, meaning to get all possible orders of them.
movesAll = []
for rightMoves in rightMovesAll:
for downMoves in downMovesAll:
interlace(rightMoves, downMoves, movesAll)
To not loose the information on a moves direction we convert all moves to moveVektor. Meanting a rightMove of 3 becomes (0,3)
and a downMove of 4 becomes (3,0)
The interlacing function (inspired by this answer) is the following
def interlace(rightMoves, downMoves, movesAll, partial=[]):
if len(rightMoves) == 0 and len(downMoves) == 0:
movesAll.append(partial)
return
else:
if len(rightMoves) > 0:
interlace(rightMoves[:-1], downMoves, movesAll, partial + [(0, rightMoves[-1])])
if len(downMoves) > 0:
interlace(rightMoves, downMoves[:-1], movesAll, partial + [(downMoves[-1], 0)])
Now we have a list of all possible combinations of moveVektors to get from (0,0)
to (N-1,M-1)
Before we can remove paths, we need to convert the list of moveVektors to the actual sequence of positions (like you provided in your example)
import operator
positionsAll = []
for vectors in vectorsAll:
positions = []
position = (0,0)
for vector in vectors:
position = tuple(map(operator.add, position, vector))
positions.append(position)
positionsAll.append(positions)
Now we need a list of all obstacle-positions so that we can check for intersections:
obstacles = [(2,2)] #example of a obstacle at 2,2
for obstacle in obstacles:
for positions in positionsAll:
if(obstacle in positions):
positionsAll.remove(positions)
This would leave us with the sequences of positions like you asked for.
Hope this works for You.
P.S. Be careful. The computational complexity grows exponentially! Also my code is 0-indexed
Upvotes: 0
Reputation: 591
Encoding the paths as a string of left (L
=incrementing rows) and right (R
=incrementing columns) steps you have a set of permutations of possible paths through the array. Then you would check, for each path, if you hit an obstacle. This could look like this (no error handling, just very limited tests performed, game board entry omitted i.e. your .
and *
are implemented as 0
and 1
for now, N=row-steps, M=col-steps, i.e. the array with the nodes has dimensions (N+1, M+1)
):
import numpy as np
import itertools
import random
def chk_pth(path, gboard):
row, col = 0, 0
for sidx, stp in enumerate(path): # step through gameboard along path
if stp == 'L':
row += 1
else:
col += 1
if gboard[row, col] == 1: # obstacle?
return False
return True
lst=[]
N, M = input().split()
N, M=int(N), int(M)
# gameboard input omitted
lst = np.zeros((N+1, M+1))
lst[random.randint(1, N), random.randint(1, M)] = 1 # hard-coded for now, todo: change to manual input
lst[N, M] = 0 # guaranteed unblocked exit
print(lst)
# N = rows, M = cols
pthstr = 'L' * N + 'R' * M # we encode the paths uniquely as lefts and right (left=row+1, right=col+1)
uq_paths = set(list(itertools.permutations(pthstr, N+M))) # only need unique paths, permutations gives multiple results
path_ok = []
for pidx, path in enumerate(uq_paths):
path_ok.append(chk_pth(path, lst))
print([pth for pidx, pth in enumerate(uq_paths) if path_ok[pidx]]) # only show valid paths
which results in (note random gameboard, first the board shown, then the valid paths):
[[0. 0. 0. 0.]
[0. 0. 1. 0.]
[0. 0. 0. 0.]
[0. 0. 0. 0.]]
[('L', 'R', 'L', 'R', 'L', 'R'), ('L', 'R', 'L', 'R', 'R', 'L'), ('L', 'R', 'L', 'L', 'R', 'R'), ('L', 'L', 'R', 'R', 'R', 'L'), ('L', 'L', 'R', 'R', 'L', 'R'), ('R', 'L', 'L', 'L', 'R', 'R'), ('L', 'L', 'R', 'L', 'R', 'R'), ('R', 'L', 'L', 'R', 'L', 'R'), ('L', 'L', 'L', 'R', 'R', 'R'), ('R', 'L', 'L', 'R', 'R', 'L'), ('R', 'R', 'R', 'L', 'L', 'L')]
Upvotes: 0