Imagine you have a full calendar year in front of you. On some days you take the train, potentially even a few times in a single day and each trip could be to a different location (I.E. The amount you pay for the ticket can be different for each trip).
So you would have data that looked like this:
Date: 2018-01-01, Amount: $5
Date: 2018-01-01, Amount: $6
Date: 2018-01-04, Amount: $2
Date: 2018-01-06, Amount: $4
...
Now you have to group this data into buckets. A bucket can span up to 31 consecutive days (no gaps) and cannot overlap another bucket.
If a bucket has less than 32 train trips it will be blue. If it has 32 or more train trips in it, it will be red. The buckets will also get a value based on the sum of the ticket cost.
After you group all the trips the blue buckets get thrown out. And the value of all the red buckets gets summed up, we will call this the prize.
The goal, is to get the highest value for the prize.
This is the problem I have. I cant think of a good algorithm to do this. If anyone knows a good way to approach this I would like to hear it. Or if you know of anywhere else that can help with designing algorithms like this.
This can be solved by dynamic programming.
First, sort the records by date, and consider them in that order.
Let day (1), day (2), ..., day (n) be the days where the tickets were bought.
Let cost (1), cost (2), ..., cost (n) be the respective ticket costs.
Let fun (k) be the best prize if we consider only the first k records.
Our dynamic programming solution will calculate fun (0), fun (1), fun (2), ..., fun (n-1), fun (n), using the previous values to calculate the next one.
Base:
fun (0) = 0.
Transition:
What is the optimal solution, fun (k), if we consider only the first k records?
There are two possibilities: either the k-th record is dropped, then the solution is the same as fun (k-1), or the k-th record is the last record of a bucket.
Let us then consider all possible buckets ending with the k-th record in a loop, as explained below.
Look at records k, k-1, k-2, ..., down to the very first record.
Let the current index be i.
If the records from i to k span more than 31 consecutive days, break from the loop.
Otherwise, if the number of records, k-i+1, is at least 32, we can solve the subproblem fun (i-1) and then add the records from i to k, getting a prize of cost (i) + cost (i+1) + ... + cost (k).
The value fun (k) is the maximum of these possibilities, along with the possibility to drop the k-th record.
Answer: it is just fun (n), the case where we considered all the records.
In pseudocode:
fun[0] = 0
for k = 1, 2, ..., n:
fun[k] = fun[k-1]
cost_i_to_k = 0
for i = k, k-1, ..., 1:
if day[k] - day[i] > 31:
break
cost_i_to_k += cost[i]
if k-i+1 >= 32:
fun[k] = max (fun[k], fun[i-1] + cost_i_to_k)
return fun[n]
It is not clear whether we are allowed to split records on a single day into different buckets.
If the answer is no, we will have to enforce it by not considering buckets starting or ending between records in a single day.
Technically, it can be done by a couple of if statements.
Another way is to consider days instead of records: instead of tickets which have day and cost, we will work with days.
Each day will have cost, the total cost of tickets on that day, and quantity, the number of tickets.
Edit: as per comment, we indeed can not split any single day.
Then, after some preprocessing to get days records instead of tickets records, we can go as follows, in pseudocode:
fun[0] = 0
for k = 1, 2, ..., n:
fun[k] = fun[k-1]
cost_i_to_k = 0
quantity_i_to_k = 0
for i = k, k-1, ..., 1:
if k-i+1 > 31:
break
cost_i_to_k += cost[i]
quantity_i_to_k += quantity[i]
if quantity_i_to_k >= 32:
fun[k] = max (fun[k], fun[i-1] + cost_i_to_k)
return fun[n]
Here, i and k are numbers of days.
Note that we consider all possible days in the range: if there are no tickets for a particular day, we just use zeroes as its cost and quantity values.
Edit2:
The above allows us to calculate the maximum total prize, but what about the actual configuration of buckets which gets us there?
The general method will be backtracking: at position k, we will want to know how we got fun (k), and transition to either k-1 if the optimal way was to skip k-th record, or from k to i-1 for such i that the equation fun[k] = fun[i-1] + cost_i_to_k holds.
We proceed until i goes down to zero.
One of the two usual implementation approaches is to store par (k), a "parent", along with fun (k), which encodes how exactly we got the maximum.
Say, if par (k) = -1, the optimal solution skips k-th record.
Otherwise, we store the optimal index i in par (k), so that the optimal solution takes a bucket of records i to k inclusive.
The other approach is to store nothing extra.
Rather, we run a slight modification code which calculates fun (k).
But instead of assigning things to fun (k), we compare the right part of the assignment to the final value fun (k) we already got.
As soon as they are equal, we found the right transition.
In pseudocode, using the second approach, and days instead of individual records:
k = n
while k > 0:
k = prev (k)
function prev (k):
if fun[k] == fun[k-1]:
return k-1
cost_i_to_k = 0
quantity_i_to_k = 0
for i = k, k-1, ..., 1:
if k-i+1 > 31:
break
cost_i_to_k += cost[i]
quantity_i_to_k += quantity[i]
if quantity_i_to_k >= 32:
if fun[k] == fun[i-1] + cost_i_to_k:
writeln ("bucket from $ to $: cost $, quantity $",
i, k, cost_i_to_k, quantity_i_to_k)
return i-1
assert (false, "can't happen")
Simplify the challenge, but not too much, to make an overlookable example, which can be solved by hand.
That helps a lot in finding the right questions.
For example take only 10 days, and buckets of maximum length of 3:
For building buckets and colorizing them, we need only the ticket count, here 0, 1, 2, 3.
On Average, we need more than one bucket per day, for example 2-0-2 is 4 tickets in 3 days. Or 1-1-3, 1-3, 1-3-1, 3-1-2, 1-2.
But We can only choose 2 red buckets: 2-0-2 and (1-1-3 or 1-3-3 or 3-1-2) since 1-2 in the end is only 3 tickets, but we need at least 4 (one more ticket than max day span per bucket).
But while 3-1-2 is obviously more tickets than 1-1-3 tickets, the value of less tickets might be higher.
The blue colored area is the less interesting one, because it doesn't feed itself, by ticket count.
Related
Imagine that we have n city in one row with the same distance and each of them has a population, we want to build a post office, and we want to choose a city that most people have to take a less route to that office, how to find the city?
The user should input the cities number(n) and their population and got the city that the post office should be built.
This is the example that was in the problem and I don't know why it has this result:
6 (number of cities (n))
3 1 0 0 2 2 (populations) ----> 2 (the city number 2 that have a 1 population)
The thing that I'm looking for is an algorithm or a formula to find the city, not the code. Any idea?
If you work out your example, you'll see a simple pattern. Let's say the total distance for index i is x. If we move to index i+1,
All cities in range [0, i] will now become further away from the post office by 1 unit.
All cities in range [i+1, n] will now become closer to the post office by 1 unit.
Using the example you have provided (assuming 0 based indexing):
Let the office be at index 1. Current total distance: 3 + (2*3) + (2*4) = 17
Let's shift it to index 2. Increase in distance for the cities on left of index 2 = 3+1=4. Decrease in distance for cities on the right = 2+2=4.
Simply put, if we move from index i to i+1,
new_distance = old_distance + sum(array[j] for j in range [0,i]) - sum(array[j] for j in range [i+1,n])
To make it more efficient, sum(array[j] for j in range [0,i]) is nothing but prefix sum. You can calculate that in one pass, then solve the distances for each city in another pass. That O(N) time and space complexity.
The problem is the famous leetcode problem (or in similar other contexts), best to buy and sell stocks, with at most k transactions.
Here is the screenshot of the problem:
I am trying to make sense of this DP solution. You can ignore the first part of large k. I don't understand the dp part why it works.
class Solution(object):
def maxProfit(self, k, prices):
"""
:type k: int
:type prices: List[int]
:rtype: int
"""
# for large k greedy approach (ignore this part for large k)
if k >= len(prices) / 2:
profit = 0
for i in range(1, len(prices)):
profit += max(0, prices[i] - prices[i-1])
return profit
# Don't understand this part
dp = [[0]*2 for i in range(k+1)]
for i in reversed(range(len(prices))):
for j in range (1, k+1):
dp[j][True] = max(dp[j][True], -prices[i]+dp[j][False])
dp[j][False] = max(dp[j][False], prices[i]+dp[j-1][True])
return dp[k][True]
I was able to drive a similar solution, but that uses two rows (dp and dp2) instead of just one row (dp in the solution above). To me it looks like the solution is overwriting values on itself, which for this solution doesn't look right. However the answer works and passes leetcode.
Lets put words to it:
for i in reversed(range(len(prices))):
For each future price we already know in advance, after considering later prices.
for j in range (1, k+1):
For each possibility of considering this price as one of k two-price transactions.
dp[j][True] = max(dp[j][True], -prices[i]+dp[j][False])
If we consider this might be a purchase -- since we are going backwards in time, a purchase means a completed transaction -- we choose the best of (1) having considered the jth purchase already (dp[j][True]) or (2) subtract this price as a purchase and add the best result we have already that includes the jth sale (-prices[i] + dp[j][False]).
dp[j][False] = max(dp[j][False], prices[i]+dp[j-1][True])
Otherwise, we might consider this as a sale (the first half of a transaction since we're going backwards), so we choose the best of (1) the jth sale already considered (dp[j][False]), or (2) we add this price as a sale and add to that the best result we have so far for the first (j - 1) completed transactions (prices[i] + dp[j-1][True]).
Note that the first dp[j][False] is referring to the jth "half-transaction," the sale if you will, since we are going backwards in time, that would have been set in an earlier iteration on a later price. We then can possibly overwrite it with our consideration of this price as a jth sale.
Consider we have a sacks of gold and thief wants to get the maximum gold. Thief can take the gold to get maximum by,
1) Taking the Gold from contiguous sacks.
2) Thief should take the same amount of gold from all sacks.
N Sacks 1 <= N <= 1000
M quantity of Gold 0 <= M <= 100
Sample Input1:
3 0 5 4 4 4
Output:
16
Explanation:
4 is the minimum amount he can take from the sacks 3 to 6 to get the maximum value of 16.
Sample Input2:
2 4 3 2 1
Output:
8
Explanation:
2 is the minimum amount he can take from the sacks 1 to 4 to get the maximum value of 8.
I approached the problem using subtracting the values from array and taking the transition point from negative to positive, but this doesn't solves the problem.
EDIT: code provided by OP to find the index:
int temp[6];
for(i=1;i<6;i++){
for(j=i-1; j>=0;j--) {
temp[j] = a[j] - a[i];
}
}
for(i=0;i<6;i++){
if(temp[i]>=0) {
index =i;
break;
}
}
The best amount of gold (TBAG) taken from every sack is equal to weight of some sack. Let's put indexes of candidates in a stack in order.
When we meet heavier weight (than stack contains), it definitely continues "good sequence", so we just add its index to the stack.
When we meet lighter weight (than stack top), it breaks some "good sequences" and we can remove heavier candidates from the stack - they will not have chance to be TBAG later. Remove stack top until lighter weight is met, calculate potentially stolen sum during this process.
Note that stack always contains indexes of strictly increasing sequence of weights, so we don't need to consider items before index at the stack top (intermediate AG) in calculation of stolen sum (they will be considered later with another AG value).
for idx in Range(Sacks):
while (not Stack.Empty) and (Sacks[Stack.Peek] >= Sacks[idx]): //smaller sack is met
AG = Sacks[Stack.Pop]
if Stack.Empty then
firstidx = 0
else
firstidx = Stack.Peek + 1
//range_length * smallest_weight_in_range
BestSUM = MaxValue(BestSUM, AG * (idx - firstidx))
Stack.Push(idx)
now check the rest:
repeat while loop without >= condition
Every item is pushed and popped once, so linear time and space complexity.
P.S. I feel that I've ever seen this problem in another formulation...
I see two differents approaches for the moment :
Naive approach: For each pair of indices (i,j) in the array, compute the minimum value m(i,j) of the array in the interval (i,j) and then compute score(i,j) = |j-i+1|*m(i,j). Take then the maximum score over all the pairs (i,j).
-> Complexity of O(n^3).
Less naive approach:
Compute the set of values of the array
For each value, compute the maximum score it can get. For that, you just have to iterate once over all the values of the array. For example, when your sample input is [3 0 5 4 4 4] and the current value you are looking is 3, then it will give you a score of 12. (You'll first find a value of 3 thanks to the first index, and then a score of 12 due to indices from 2 to 5).
Take the maximum over all values found at step 2.
-> Complexity is here O(n*m), since you have to do at most m times the step 2, and the step 2 can be done in O(n).
Maybe there is a better complexity, but I don't have a clue yet.
I have a search result from two way flight. So, there are two lists that contain the departure flights and arrival flights such as:
The departure flights list has 20 flights.
The arrival flights list has 30 flights
So, I will have 600 (20*30) combination between departure flight and arrival flight. I will call the combination list is the result list
However, I just want to select a limitation from 600 combination. For instance, I will select the best of 100 flight combination. The criteria to combine the flights is the cheap price for departure and arrival flight.
To do that, I will sort the result list by the total price of departure and arrival flight. And I then pick up the first 100 elements from result list to get what I want.
But, if the departure flights list has 200 flights and arrival flights list has 300 flights, I will have the result list with 60.000 elements. For that reason, I will sort a list with 60.000 elements to find the best 100 elements.
So, there is any an algorithm to select the best combinations as my case.
Thank you so much.
Not 100% clear from your question, but I understand that you are looking for a faster algorithm to find a certain number of best / cheapest combinations of departure and arrival flights.
You can do this much faster by sorting the lists of departure and arrival flights individually by cost and then using a heap for expanding the next-best combinations one-by-one until you have enough.
Here's the full algorithm -- in Python, but without using any special libraries, just standard data structures, so this should be easily transferable to any other language:
NUM_FLIGHTS, NUM_BEST = 1000, 100
# create test data: each entry corresponds to just the cost of one flight
from random import randint
dep = sorted([randint(1, 100) for i in range(NUM_FLIGHTS)])
arr = sorted([randint(1, 100) for i in range(NUM_FLIGHTS)])
def is_compatible(i, j): # for checking constraints, e.g. timing of flights
return True # but for now, assume no constraints
# get best combination using sorted lists and heap
from heapq import heappush, heappop
heap = [(dep[0] + arr[0], 0, 0)] # initial: best combination from dep and arr
result = [] # the result list
visited = set() # make sure not to add combinations twice
while heap and len(result) < NUM_BEST:
cost, i, j = heappop(heap) # get next-best combination
if (i, j) in visited: continue # did we see those before? skip
visited.add((i, j))
if is_compatible(i, j): # if 'compatible', add to results
result.append((cost, dep[i], arr[j]))
# add 'adjacent' combinations to the heap
if i < len(dep) - 1: # next-best departure + same arrival
heappush(heap, (dep[i+1] + arr[j], i+1, j))
if j < len(arr) - 1: # same departure + next-best arrival
heappush(heap, (dep[i] + arr[j+1], i, j+1))
print result
# just for testing: compare to brute-force (get best from all combinations)
comb = [(d, a) for d in dep for a in arr]
best = sorted((d+a, d, a) for (d, a) in comb)[:NUM_BEST]
print best
print result == best # True -> same results as brute force (just faster)
This works roughly like this:
sort both the departure flights dep and the arrival flights arr by their cost
create a heap and put the best combination (best departure and best arrival) as well as the corresponding indices in their lists into the heap: (dep[0] + arr[0], 0, 0)
repeat until you have enough combinations or there are no more elements in the heap:
pop the best element from the heap (sorted by total cost)
if it satisfies the contraints, add it to the result set
make sure you do not add flights twice to the result set, using visited set
add the two 'adjacent' combinations to the heap, i.e. taking the same flight from dep and the next from arr, and the next from dep and the same from arr, i.e. (dep[i+1] + arr[j], i+1, j) and (dep[i] + arr[j+1], i, j+1)
Here's a very small worked example. The axes are (the costs of) the dep and arr flights, and the entries in the table are in the form n(c)m, where n is the iteration that entry was added to the heap (if it is at all), c is the cost, and m is the iteration it was added to the 'top 10' result list (if any).
dep\arr 1 3 4 6 7
2 0(3)1 1(5)4 4(6)8 8(8)- -
2 1(3)2 2(5)6 6(6)9 9(8)- -
3 2(4)3 3(6)7 7(7)- - -
4 3(5)5 5(7)- - - -
6 5(7)10 - - - -
Result: (1,2), (1,2), (1,3), (3,2), (1,4), (3,2), (3,3), (2,4), (2,4), (1,6)
Note how the sums in both the columns and the rows of the matrix are always increasing, so the best results can always be found in a somewhat triangular area in the top-left. Now the idea is that if your currently best combination (the one that's first in the heap) is dep[i], arr[i], then there's no use in checking, e.g., combination dep[i+2], arr[i] before checking dep[i+1], arr[i], which must have a lower total cost, so you add dep[i+1], arr[i] (and likewise dep[i], arr[i+1]) to the heap, and repeat with popping the next element from the heap.
I compared the results of this algorithm to the results of your brute-force approach, and the resulting flights are the same, i.e. the algorithm works, and always yields the optimal result. Complexity should be O(n log(n)) for sorting the departure and arrival lists (n being the number of flights in those original lists), plus O(m log(m)) for the heap-loop (m iterations with log(m) work per iteration, m being the number of elements in the result list).
This finds the best 1,000 combinations of 100,000 departure and 100,000 arrival flights (for a total of 1,000,000,000,000 possible combinations) in less than one second.
Note that those numbers are for the case that you have no additional constraints, i.e. each departure flight can be combined with each arrival flight. If there are constraints, you can use the is_compatible function sketched in the above code to check those and to skip that pairing. This means, that for each incompatible pair with low total cost, the loop needs one additional iteration. This means that in the worst case, for example if there are no compatible pairs at all, or when the only compatible pairs are those with the highest total cost, the algorithm could in fact expand all the combination.
On average, though, this should not be the case, and the algorithm should perform rather quickly.
I think the best solution would be using some SQL statements to do the Cartesian product. You can apply any kind of filters, based on the data itself, ordering, range selection, etc. Something like this:
SELECT d.time as dep_time, a.time as arr_time, d.price+a.price as total_price
FROM departures d, arrivals a
WHERE a.time > d.time + X
ORDER BY d.price+a.price
LIMIT 0,100
Actually X can be even 0, but arrival should happen AFTER the departure anyways.
Why I would choose SQL:
It's closest to the data itself, you don't have to query them
It's highly optimized, if you use indexes, I'm sure you can't beat its performance with your own code
It's simple and declarative :)
I'm implementing an m,n,k-game, a generalized version of tic-tac-toe, where m is the number of rows, n is the number of columns and k is the number of pieces that a player needs to put in a row to win. I have implemented a check for a win, but I haven't figured out a satisfactory way to check before the board is full of pieces, if no player can win the game. In other words, there might be empty slots on the board, but they cannot be filled in such a way that one player would win.
My question is, how to check this efficiently? The following algorithm is the best that I can think of. It checks for two conditions:
A. Go over all board positions in all 4 directions (top to bottom, right to left, and both diagonal directions). If say k = 5, and 4 (= k-1) consecutive empty slots are found, stop checking and report "no tie". This doesn't take into account for example the following situation:
OX----XO (Example 1)
where a) there are 4 empty consecutive slots (-) somewhere between two X's, b) next it is O's turn, c) there are less than four other empty positions on the board and no player can win by putting pieces to those, and d) it is not possible to win in any other direction than horizontally in the shown slots either. Now we know that it is a tie because O will eventually block the last winning possibility, but erroneously it is not reported yet because there are four consecutive empty slots. That would be ok (but not great). Checking this condition gives a good speed-up at the beginning when the checking algorithm usually finds such a case early, but it gets slower as more pieces are put on the board.
B. If this k-1-consecutive-empty-slots-condition isn't met, the algorithm would check the slots again consecutively in all 4 directions. Suppose we are currently checking from left to right. If at some point an X is encountered and it was preceded by an O or - (empty slot) or a board border, then start counting the number of consecutive X's and empty slots, counting in this first encountered X. If one can count to 5, then one knows it is possible for X to win, and "no tie" is reported. If an O preceded by an X is encountered before 5 consecutive X's, then X cannot win in those 5 slots from left to right starting from where we started counting. For example:
X-XXO (Example 2)
12345
Here we started checking at position 1, counted to 4, and encountered an O. In this case, one would continue from the encountered O in the same way, trying to find 5 consecutive O's or empty slots this time. In another case when counting X's or empty slots, an O preceded by one or more empty slots is encountered, before counting to 5. For example:
X-X-O (Example 3)
12345
In this case we would again continue from the O at position 5, but add to the new counter (of consecutive O's or empty slots) the number of consecutive empty slots that preceded O, here 1, so that we wouldn't miss for example this possible winning position:
X-X-O---X (Example 4)
In this way, in the worst case, one would have to go through all positions 4 times (4 directions, and of course diagonals whose length is less than k can be skipped), giving running time O(mn).
The best way I could think of was doing these two described checks, A and B, in one pass. If the checking algorithm gets through all positions in all directions without reporting "no tie", it reports a tie.
Knowing that you can check a win just by checking in the vicinity of the last piece that was added with running time O(k), I was wondering if there were quicker ways to do an early check for a tie. Doesn't have to be asymptotically quicker. I'm currently keeping the pieces in a two-dimensional array. Is there maybe a data structure that would allow an efficient check? One approach: what is the highest threshold of moves that one can wait the players to make before running any checks for a tie at all?
There are many related questions at Stack Overflow, for example this, but all discussions I could find either only pointed out the obvious tie condition, where the number of moves made is equal to the size of the board (or they checked if the board is full), or handled only the special case where the board is square: m = n. For example this answer claims to do the check for a tie in constant time, but only works when m = n = k. I'm interested in reporting the tie as early as possible and for general m,n and k. Also if the algorithm works for more than two players, that would be neat.
I would reduce the problem of determining a tie to the easier sub-problem:
Can player X still win?
If the answer is 'no' for all players, it is a tie.
To find out whether Player X can win:
fill all blank spaces with virtual 'X'-pieces
are there k 'X'-pieces in a row anywhere?
if there are not --> Player X cannot win. return false.
if there are, find the row of k stones with the least amount of virtual pieces. Count the number of virtual pieces in it.
count the number of moves player X has left, alternating with all other players, until the board is completely full.
if the number of moves is less than the amount of virtual pieces required to win, player X cannot win. return false.
otherwise, player X can still win. return true.
(This algorithm will report a possible win for player X even in cases where the only winning moves for X would have another player win first, but that is ok, since that would not be a tie either)
If, as you said, you can check a win just by checking in the vicinity of the last piece that was added with running time O(k), then I think you can run the above algorithm in O(k * Number_of_empty_spots): Add all virtual X-Piece, note any winning combinations in the vicinity of the added pieces.
The number of empty slots can be large, but as long as there is at least one empty row of size k and player X has still k moves left until the board is filled, you can be sure that player X can still win, so you do not need to run the full check.
This should work with any number of players.
Actually the constant time solution you referenced only works when k = m = n as well. If k is smaller then I don't see any way to adapt the solution to get constant time, basically because there are multiple locations on each row/column/diagonal where a winning consecutive k 0's or 1's may occur.
However, maintaining auxiliary information for each row/column/diagonal can give a speed up. For each row/column/diagonal, you can store the start and end locations for consecutive occurrences of 1's and blanks as possible winning positions for player 1, and similarly store start and end locations of consecutive occurrences of 0's and blanks as possible winning positions for player 0. Note that for a given row/column/diagonal, intervals for player 0 and 1 may overlap if they contain blanks. For each row/column/diagonal, store the intervals for player 1 in sorted order in a self-balancing binary tree (Note you can do this because the intervals are disjoint). Similarly store the intervals for player 0 sorted in a tree. When a player makes a move, find the row/column/diagonals that contain the move location and update the intervals containing the move in the appropriate row column and diagonal trees for the player that did not make the move. For the player that did not make a move, this will split an interval (if it exists) into smaller intervals that you can replace the old interval with and then rebalance the tree. If an interval ever gets to length less than k you can delete it. If a tree ever becomes empty then it is impossible for that player to win in that row/column/diagonal. You can maintain a counter of how many rows/columns/diagonals are impossible to win for each player, and if the counter ever reaches the total number of rows/columns/diagonals for both players then you know you have a tie. The total running time for this is O(log(n/k) + log(m/k)) to check for a tie per move, with O(mn/k) extra space.
You can similarly maintain trees that store consecutive intervals of 1's (without spaces) and update the trees in O(log n + log m) time when a move is made, basically searching for the positions before and after the move in your tree and updating the interval(s) found and merging two intervals if two intervals (before and after) are found. Then you report a win if an interval is ever created/updated and obtains length greater than or equal to k. Similarly for player 0. Total time to check for a win is O(log n + log m) which may be better than O(k) depending on how large k is. Extra space is O(mn).
Let's look at one row (or column or diagonal, it doesn't matter) and count the number of winning lines of length k ("k-line") it's possible to make, at each place in the row, for player X. This solution will keep track of that number over the course of the game, checking fulfillment of the winning condition on each move as well as detecting a tie.
1 2 3... k k k k... 3 2 1
There is one k-line including an X in the leftmost slot, two with the second slot from the left, and so on. If an opposing player, O or otherwise, plays in this row, we can reduce the k-line possibility counts for player X in O(k) time at the time of the move. (The logic for this step should be straightforward after doing an example, needing no other data structure, but any method involving checking each of the k rows of k from will do. Going left to right, only k operations on the counts is needed.) An enemy piece should set the possibility count to -1.
Then, a detectably tied game is one where no cell has a non-zero k-line possibility count for any player. It's easy to check this by keeping track of the index of the first non-zero cell. Maintaining the structure amounts to O(k*players) work on each move. The number of empty slots is less than those filled, for positions that might be tied, so the other answers are good for checking a position in isolation. However, at least for reasonably small numbers of players, this problem is intimately linked with checking the winning condition in the first place, which at minimum you must do, O(k), on every move. Depending on your game engine there may be a better structure that is rich enough to find good moves as well as detect ties. But the possibility counting structure has the nice property that you can check for a win whilst updating it.
If space isn't an issue, I had this idea:
For each player maintain a structure sized (2mn + (1 - k)(m + n) + 2(m - k + 1)(n - k + 1) + 2(sum 1 to (m - k))) where each value represents if one of another player's moves are in one distinct k-sized interval. For example for a 8-8-4 game, one element in the structure could represent row 1, cell 0 to 3; another row 1, cell 1 to 4; etc.
In addition, one variable per player will represent how many elements in their structure are still unset. Only one move is required to set an element, showing that that k-interval can no longer be used to win.
An update of between O(k) and O(4k) time per player seems needed per move. A tie is detected when the number of players exceeds the number of different elements unset.
Using bitsets, the number of bytes needed for each player's structure would be the structure size divided by 8. Notice that when k=m=n, the structure size is 4*k and update time O(4). Less than half a megabyte per player would be needed for a 1000,1000,5 game.
Below is a JavaScript example.
var m = 1000, n = 1000, k = 5, numberOfPlayers = 2
, numberOfHorizontalKIs = m * Math.max(n - k + 1,0)
, numberOfverticalKIs = n * Math.max(m - k + 1,0)
, horizontalVerticalKIArraySize = Math.ceil((numberOfHorizontalKIs + numberOfverticalKIs)/31)
, horizontalAndVerticalKIs = Array(horizontalVerticalKIArraySize)
, numberOfUnsetKIs = horizontalAndVerticalKIs
, upToM = Math.max(0,m - k) // southwest diagonals up to position m
, upToMSum = upToM * (upToM + 1) / 2
, numberOfSouthwestKIs = 2 * upToMSum //sum is multiplied by 2 to account for bottom-right-corner diagonals
+ Math.max(0,n - m + 1) * (m - k + 1)
, diagonalKIArraySize = Math.ceil(2 * numberOfSouthwestKIs/31)
, diagonalKIs = Array(diagonalKIArraySize)
, numberOfUnsetKIs = 2 * numberOfSouthwestKIs + numberOfHorizontalKIs + numberOfverticalKIs
function checkTie(move){
var row = move[0], column = move[1]
//horizontal and vertical
for (var rotate=0; rotate<2; rotate++){
var offset = Math.max(k - n + column, 0)
column -= offset
var index = rotate * numberOfHorizontalKIs + (n - k + 1) * row + column
, count = 0
while (column >= 0 && count < k - offset){
var KIArrayIndex = Math.floor(index / 31)
, bitToSet = 1 << index % 31
if (!(horizontalAndVerticalKIs[KIArrayIndex] & bitToSet)){
horizontalAndVerticalKIs[KIArrayIndex] |= bitToSet
numberOfUnsetKIs--
}
index--
column--
count++
}
//rotate board to log vertical KIs
var mTmp = m
m = n
n = mTmp
row = move[1]
column = move[0]
count = 0
}
//rotate board back
mTmp = m
m = n
n = mTmp
// diagonals
for (var rotate=0; rotate<2; rotate++){
var diagonalTopColumn = column + row
if (diagonalTopColumn < k - 1 || diagonalTopColumn >= n + m - k){
continue
} else {
var offset = Math.max(k - m + row, 0)
row -= offset
column += offset
var dBeforeM = Math.min (diagonalTopColumn - k + 1,m - k)
, dAfterM = n + m - k - diagonalTopColumn
, index = dBeforeM * (dBeforeM + 1) / 2
+ (m - k + 1) * Math.max (Math.min(diagonalTopColumn,n) - m + 1,0)
+ (diagonalTopColumn < n ? 0 : upToMSum - dAfterM * (dAfterM + 1) / 2)
+ (diagonalTopColumn < n ? row : n - 1 - column)
+ rotate * numberOfSouthwestKIs
, count = 0
while (row >= 0 && column < n && count < k - offset){
var KIArrayIndex = Math.floor(index / 31)
, bitToSet = 1 << index % 31
if (!(diagonalKIs[KIArrayIndex] & bitToSet)){
diagonalKIs[KIArrayIndex] |= bitToSet
numberOfUnsetKIs--
}
index--
row--
column++
count++
}
}
//mirror board
column = n - 1 - column
}
if (numberOfUnsetKIs < 1){
return "This player cannot win."
} else {
return "No tie."
}
}