Topcoder SRM 343, D2, 1000-point problem
James S. Plank
CS302
November, 2012
Problem description: http://community.topcoder.com/stat?c=problem_statement&pm=7509&rd=10667.
The driver for this is in
Mafia-Main.cpp, which includes Mafia.cpp
and compiles the example cases.
The problem description is fairly long, but go through example 0 so that you understand
the basic mechanics.
Using Dynamic Programming -- Spotting the Recursion
As with many approaches that involve dynamic programming, the key is to spot
the recursion. The first approach I'd take in this problem is a very brain-dead
recursive approach. If you start during the day, there's no choice -- you simply find
the most guilty player and move on. However, during the evening, you try all
of the choices for removing players, by removing a player and then calling the game
recursively.
How do you "call the game recursively?" Well, the first thing you could do is create
new guilt and responses vectors, and call play() recursively
on those. However, that's a lot of work, and as you'll see later, it's harder to
memoize. Instead, let's represent the state of a game by a string, which has
N characters, one for each player in the game. The character for each
player will be:
- '0' if the player is still in the game.
- '1' if the player has been removed by the Mafia
- '2' if the player has been removed by the villagers.
Think to yourself -- if I have this string, plus the original guilt and responses
vectors, can I calculate every survivor's guilt at this point in the game, regardless
of the order in which people were removed from the game? The answer to that is "Yes":
Suppose player i is a survivor and players j and k have been removed
by the Mafia. Then player i's guilt is going to be equal to:
guilt[i]
+ responses[j][i]
+ responses[k][i]
That's assuming of course, that you've converted responses to vectors of
integers instead of those stupid strings.
This string that represents the game's state is important,
because it gives us something on which to memoize.
Let's work through Example 0 with the string. The first thing we do is add a few
variables to our class. The first is a vector of
vectors of integers called R, which converts responses to integers. In
Example 0, this will be:
R = { { 1, 4, 3, -2 },
{ -2, 1, 4, 3 },
{ 3, -2, 1, 4 },
{ 4, 3, -2, 1 } }
The second is G, which is simply a copy of the original guilt, and
the third is Me, which is a copy of playerIndex.
Then we add a method int RPlay(string state), which returns the maximum number of
rounds that we can play given the string that represents the game state. The first
call we'll make is the initial one: RPlay("0000").
Now, let's go through all the recursive calls that will be made:
- RPlay("0000") will recursively call:
RPlay("1000"),
RPlay("0010") and
RPlay("0001").
It won't call "RPlay("0100") because that kills Me.
It will return the maximum of its recursive calls, plus one.
- RPlay("1000") calculates guilts and determine that player 1 must be removed.
Therefore, it returns 0 (since it is a "daytime") state, and you want to return the number of
nights during which Me survives.
- RPlay("0010") calculates guilts and determines that player 3 must be removed.
So, it recursively calls RPlay("0012") and will return what it returns.
- RPlay("0001") calculates guilts and determines that player 2 must be removed.
So, it recursively calls RPlay("0021") and will return what it returns.
- RPlay("0012") calls RPlay("1012"), and will return what it returns, plus one.
- RPlay("0021") calls RPlay("1021"), and will return what it returns, plus one.
- RPlay("1012") only has one player left, so it will return 0.
- RPlay("1021") only has one player left, so it will return 0.
- This means that both RPlay("0012") and RPlay("0021") return 1.
- This means that both RPlay("0010") and RPlay("0001") return 1.
- This means that RPlay("0000") returns 2.
This example didn't feature any duplicate calls, but you can see how they might arise. Therefore,
you memoize on the key (use a map).
Have fun!