# Finding sum-free partitions

## Executive summary

Given input k, find a partition of integers 1 to n into k sum-free subsets for the largest n you can within 10 minutes.

## Background: Schur numbers

A set A is sum-free if its self-sum A + A = { x + y | x, y in A} has no elements in common with it.

For every positive integer k there is a largest integer S(k) such that the set {1, 2, ..., S(k)} can be partitioned into k sum-free subsets. This number is called the kth Schur number (OEIS A045652).

For example, S(2) = 4. We can partition {1, 2, 3, 4} as {1, 4}, {2, 3}, and that is the unique partition into two sum-free subsets, but we can't now add a 5 to either part.

## Challenge

Write a deterministic program which does the following:

• Take a positive integer k as input
• Write the current Unix timestamp to stdout
• Outputs a sequence of partitions of 1 to n into k sum-free subsets for increasing n, following each sequence with the current Unix timestamp.

The winner will be the program which prints a partition for the largest n within 10 minutes on my computer when given input 5. Ties will be broken by the quickest time to find a partition for the largest n, averaged over three runs: that's why the output should include timestamps.

Important details:

• I have Ubuntu Precise, so if your language is not supported I will be unable to score it.
• I have an Intel Core2 Quad CPU, so if you want to use multithreading there's no point using more than 4 threads.
• If you want me to use any particular compiler flags or implementation, document that clearly in your answer.
• You shall not special-case your code to handle the input 5.
• You are not required to output every improvement you find. E.g. for input 2 you could output only the partition for n = 4. However, if you don't output anything in the first 10 minutes then I will score that as n = 0.

## Python 3, sort by greatest number, n = 92 121

Thanks to Martin Büttner for a suggestion that unexpectedly improved the maximum n reached.

Last output:

[2, 3, 11, 12, 29, 30, 38, 39, 83, 84, 92, 93, 110, 111, 119, 120]
[1, 4, 10, 13, 28, 31, 37, 40, 82, 85, 91, 94, 109, 112, 118, 121]
[5, 6, 7, 8, 9, 32, 33, 34, 35, 36, 86, 87, 88, 89, 90, 113, 114, 115, 116, 117]
[14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108]
[41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81]


The algorithm is the same as my previous answer, quoted below:

There are k bins that have both the numbers in it so far and the numbers that can't go in it anymore. At each depth in the iteration (it's basically a depth-first search), the bin ordering is shuffled and the next number (nextN) is (sequentially) put into the bins that can take it and then goes one step deeper. If there are none, it returns, backing up one step.

...with one exception: the bin ordering is not shuffled. Instead, it is sorted in such a way that the bins with the greatest number come first. This reached n = 121 in 8 seconds!

Code:

from copy import deepcopy
from random import shuffle, seed
from time import clock, time
global maxN
maxN = 0
clock()

def search(k,nextN=1,sets=None):
global maxN
if clock() > 600: return

if nextN == 1: #first iteration
sets = []
for i in range(k):
sets.append([[],[]])

sets.sort(key=lambda x:max(xor), reverse=True)
for i in range(k):
if clock() > 600: return
if nextN not in sets[i]:
sets2 = deepcopy(sets)
sets2[i].append(nextN)
sets2[i].extend([nextN+j for j in sets2[i]])
nextN2 = nextN + 1

if nextN > maxN:
maxN = nextN
print("New maximum!",maxN)
for s in sets2: print(s)
print(time())
print()

search(k, nextN2, sets2)

search(5)

• Note: sorting by greatest number of allowed numbers within the range of disallowed numbers gives n=59, and sorting by greatest number of allowed numbers less than nextN gives n=64. Sorting by the length of the disallowed numbers list (which may have repeats) leads very quickly to an elegant n=30 pattern. – El'endia Starman Nov 9 '15 at 23:39
• The output time format isn't correct (should be seconds since the epoch, but I'm seeing Tue Nov 10 00:44:25 2015), but I saw n=92 in less than 2 seconds. – Peter Taylor Nov 9 '15 at 23:46
• Ah, I figured that the time format wasn't as important as showing exactly how long it took. I'll figure it out and change it though. EDIT: D'oh. I picked ctime over time because the output was prettier when time was exactly what I should've picked. – El'endia Starman Nov 10 '15 at 1:44
• You know, you could also just sort by greatest number in the bin, because the greatest disallowed number will always be twice that. – Martin Ender Nov 10 '15 at 8:05
• @MartinBüttner: ......I...uh...I have no idea how or why, but that gets n=121. o.O – El'endia Starman Nov 10 '15 at 8:21

# Python 3, 121, < 0.001s

Improved heuristic thanks to Martin Buttner means we don't even need randomness.

Output:

1447152500.9339304
[1, 4, 10, 13, 28, 31, 37, 40, 82, 85, 91, 94, 109, 112, 118, 121]
[2, 3, 11, 12, 29, 30, 38, 39, 83, 84, 92, 93, 110, 111, 119, 120]
[5, 6, 7, 8, 9, 32, 33, 34, 35, 36, 86, 87, 88, 89, 90, 113, 114, 115, 116, 117]
[14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108]
[41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81]
1447152500.934646 121


Code:

from copy import deepcopy
from random import seed, randrange
from time import clock, time
from cProfile import run

n = 5

seed(0)

def heuristic(bucket):
return len(bucket) and bucket[-1]

def search():
best = 0
lists = [[[],set()] for _ in range(n)]
print(time())
lists.sort(key=heuristic, reverse=True)
for i in range(n):
lists[i].update([next_add + old for old in lists[i]])
break

for l in lists:
print(l)

search()


# Python 3, 112

## Sort by sum of first 2 elements + skew

[40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79]
[7, 8, 9, 10, 11, 12, 13, 27, 28, 29, 30, 31, 32, 33, 80, 81, 82, 83, 84, 85, 86, 100, 101, 102, 103, 104, 105, 106]
[3, 4, 14, 19, 21, 26, 36, 37, 87, 92, 94, 99, 109, 110]
[2, 5, 16, 17, 20, 23, 24, 35, 38, 89, 90, 96, 97, 108, 111]
[1, 6, 15, 18, 22, 25, 34, 39, 88, 91, 93, 95, 98, 107, 112]
1447137688.032085 138.917074 112


I copied El'endia Starman's data structure, which consists of a list of pairs, where the first element of the pair is the elements in that bucket, and the second is the sums of that bucket.

I start with the same "track what sums are availible" approach. My sorting heuristic is simply the sum of the smallest two elements in a given list. I also add a small random skew to try different possibilities.

Each iteration simply places each the new number in the first avalible bin, similar to random greedy. Once this fails, it simply restarts.

from copy import deepcopy
from random import seed, randrange
from time import clock, time

n = 5

seed(0)

def skew():
return randrange(9)

best = 0
while clock() < 600:
lists = [[[],[]] for _ in range(n)]
lists.sort(key=lambda x:sum(x[:2]) + skew(), reverse=True)
for i in range(n):
lists[i].extend([next_add + old for old in lists[i]])
for l in lists:
print(l)
break

• Wow, this looks extremely similar to my code. :P ;) (I don't mind at all.) – El'endia Starman Nov 10 '15 at 7:26
• @El'endiaStarman Credit added. It's a nice basis. – isaacg Nov 10 '15 at 7:30

# Java 8, n = 142 144

Last output:

@ 0m 31s 0ms
n: 144
[9, 12, 17, 20, 22, 23, 28, 30, 33, 38, 41, 59, 62, 65, 67, 70, 72, 73, 75, 78, 80, 83, 86, 91, 107, 115, 117, 122, 123, 125, 128, 133, 136]
[3, 8, 15, 24, 25, 26, 31, 35, 45, 47, 54, 58, 64, 68, 81, 87, 98, 100, 110, 114, 119, 120, 121, 130, 137, 142]
[5, 13, 16, 19, 27, 36, 39, 42, 48, 50, 51, 94, 95, 97, 103, 106, 109, 112, 118, 126, 129, 132, 138, 140, 141]
[2, 6, 11, 14, 34, 37, 44, 53, 56, 61, 69, 76, 79, 84, 89, 92, 101, 104, 108, 111, 124, 131, 134, 139, 143, 144]
[1, 4, 7, 10, 18, 21, 29, 32, 40, 43, 46, 49, 52, 55, 57, 60, 63, 66, 71, 74, 77, 82, 85, 88, 90, 93, 96, 99, 102, 105, 113, 116, 127, 135]


Performs a seeded random search distributed over 4 threads. When it can't find a partition to fit n in it tries to free up space for n in one partition at a time by dumping as much as it can out of it into the other partitions.

edit: tweaked the algorithm for freeing up space for n, also added ability to go back to a previous choice and choose again.

note: the output is not strictly deterministic because there are multiple threads involved and they can end up updating the best n found so far in jumbled order; the eventual score of 144 is deterministic though and is reached fairly quickly: 30 seconds on my computer.

Code is:

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

public class SumFree {

private static int best;

public static void main(String[] args) {
int k = 5; // Integer.valueOf(args);

long start = System.currentTimeMillis();
long end = start + TimeUnit.MINUTES.toMillis(10);

System.out.println(start);

Random rand = new Random("Lucky".hashCode());
for (int i = 0; i < numThreadsPeterTaylorCanHandle; i++) {
new Thread(() -> search(k, new Random(rand.nextLong()), start, end)).start();
}
}

private static void search(int k, Random rand, long start, long end) {
long now = System.currentTimeMillis();
int localBest = 0;

do {
// create k empty partitions
List<Partition> partitions = new ArrayList<>();
for (int i = 0; i < k; i++) {
}

Deque<Choice> pastChoices = new ArrayDeque<>();
int bestNThisRun = 0;

// try to fill up the partitions as much as we can
for (int n = 1;; n++) {
// list of partitions that can fit n
List<Partition> partitionsForN = new ArrayList<>(k);
for (Partition partition : partitions) {
if (!partition.sums.contains(n)) {
}
}

// if we can't fit n anywhere then try to free up some space
// by rearranging partitions
Set<Set<Set<Integer>>> rearrangeAttempts = new HashSet<>();
rearrange: while (partitionsForN.size() == 0 && rearrangeAttempts

Collections.shuffle(partitions, rand);
for (int candidateIndex = 0; candidateIndex < k; candidateIndex++) {
// partition we will try to free up
Partition candidate = partitions.get(candidateIndex);
// try to dump stuff that adds up to n into the other
// partitions
for (int candidateElement : candidate.elements) {
if (candidate.elements.contains(n - candidateElement)) {
}
}
for (int i = 0; i < k && !badElements.isEmpty(); i++) {
if (i == candidateIndex) {
continue;
}

Partition other = partitions.get(i);

for (int j = 0; j < badElements.size(); j++) {
if (!other.sums.contains(candidateElement)
&& !other.elements.contains(candidateElement + candidateElement)) {
boolean canFit = true;
for (int otherElement : other.elements) {
if (other.elements.contains(candidateElement + otherElement)) {
canFit = false;
break;
}
}

if (canFit) {
for (int otherElement : other.elements) {
}
candidate.elements.remove((Integer) candidateElement);
}
}
}
}

// recompute the sums
candidate.sums.clear();
List<Integer> elementList = new ArrayList<>(candidate.elements);
int elementListSize = elementList.size();
for (int i = 0; i < elementListSize; i++) {
int ithElement = elementList.get(i);
for (int j = i; j < elementListSize; j++) {
int jthElement = elementList.get(j);
}
}

// if candidate can now fit n then we can go on
if (!candidate.sums.contains(n)) {
break rearrange;
}
}
}

// if we still can't fit in n, then go back in time to our last
// choice (if it's saved) and this time choose differently
if (partitionsForN.size() == 0 && !pastChoices.isEmpty() && bestNThisRun > localBest - localBest / 3) {
Choice lastChoice = pastChoices.peek();
partitions = new ArrayList<>(lastChoice.partitions.size());
for (Partition partition : lastChoice.partitions) {
}
n = lastChoice.n;
Partition partition = lastChoice.unchosenPartitions
.get(rand.nextInt(lastChoice.unchosenPartitions.size()));
lastChoice.unchosenPartitions.remove(partition);
partition = partitions.get(lastChoice.partitions.indexOf(partition));
for (int element : partition.elements) {
}
if (lastChoice.unchosenPartitions.size() == 0) {
pastChoices.pop();
}
continue;
}

if (partitionsForN.size() > 0) {
// if we can fit in n somewhere,
// pick that somewhere randomly
Partition chosenPartition = partitionsForN.get(rand.nextInt(partitionsForN.size()));
// if we're making a choice then record it so that we may
if (partitionsForN.size() > 1) {
Choice choice = new Choice();
choice.n = n;
for (Partition partition : partitions) {
}
for (Partition partition : partitionsForN) {
if (partition != chosenPartition) {
}
}
pastChoices.push(choice);

// only keep 3 choices around
if (pastChoices.size() > 3) {
pastChoices.removeLast();
}
}

for (int element : chosenPartition.elements) {
}
bestNThisRun = Math.max(bestNThisRun, n);
}

if (bestNThisRun > localBest) {
localBest = Math.max(localBest, bestNThisRun);

synchronized (SumFree.class) {
now = System.currentTimeMillis();

if (bestNThisRun > best) {
// sanity check
Set<Integer> allElements = new HashSet<>();
for (Partition partition : partitions) {
for (int e1 : partition.elements) {
throw new RuntimeException("Oops!");
}
for (int e2 : partition.elements) {
if (partition.elements.contains(e1 + e2)) {
throw new RuntimeException("Oops!");
}
}
}
}
if (allElements.size() != bestNThisRun) {
throw new RuntimeException("Oops!" + allElements.size() + "!=" + bestNThisRun);
}

best = bestNThisRun;
System.out.printf("@ %dm %ds %dms\n", TimeUnit.MILLISECONDS.toMinutes(now - start),
TimeUnit.MILLISECONDS.toSeconds(now - start) % 60, (now - start) % 1000);
System.out.printf("n: %d\n", bestNThisRun);
for (Partition partition : partitions) {
// print in sorted order since everyone else
// seems to to that
List<Integer> partitionElementsList = new ArrayList<>(partition.elements);
Collections.sort(partitionElementsList);
System.out.println(partitionElementsList);
}
System.out.printf("timestamp: %d\n", now);
System.out.println("------------------------------");
}
}
}

if (partitionsForN.size() == 0) {
break;
}
}
} while (now < end);
}

// class representing a partition
private static final class Partition {

// the elements of this partition
Set<Integer> elements = new HashSet<>();

// the sums of the elements of this partition
Set<Integer> sums = new HashSet<>();

Partition() {
}

Partition(Partition toCopy) {
}

Set<Integer> getElements() {
return elements;
}
}

private static final class Choice {
int n;
List<Partition> partitions = new ArrayList<>();
List<Partition> unchosenPartitions = new ArrayList<>();
}
}


## C, Random Greedy, n = 91

Just to provide a baseline solution, this iterates over n, keeping track of the bins and their sums and adds n to a random bin where it doesn't appear as a sum yet. It terminates once n appears in all k sums, and if the resulting n was better than any previous attempt, prints it to STDOUT.

The input k is provided via a command-line argument. The maximum possible k is currently hardcoded to 10 because I was too lazy add dynamic memory allocation, but that could be fixed easily.

I guess I could go hunting for a better seed now, but this answer is probably not particularly competitive anyway, so meh.

Here is the partition for n = 91:

1 5 12 18 22 29 32 35 46 48 56 59 62 69 72 76 79 82 86 89
2 3 10 11 16 17 25 30 43 44 51 52 57 64 71 83 84 90 91
6 8 13 15 24 31 33 38 40 42 49 54 61 63 65 77 81 88
9 14 19 21 27 34 37 45 60 67 70 73 75 78 80 85
4 7 20 23 26 28 36 39 41 47 50 53 55 58 66 68 74 87


And finally, here is the code:

#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#define MAX_K 10
#define MAX_N 1024

int main(int argc, char **argv) {
if (argc < 2)
{
printf("Pass in k as a command-line argument");
return 1;
}

printf("%u\n", (unsigned)time(NULL));

int k = atoi(argv);

int sizes[MAX_K];
int bins[MAX_K][MAX_N];
int sums[MAX_K][2*MAX_N];
int selection[MAX_K];
int available_bins;

int best = 0;

srand(1447101176);

while (1)
{
int i,j;
for (i = 0; i < k; ++i)
sizes[i] = 0;
for (i = 0; i < k*MAX_N; ++i)
bins[i] = 0;
for (i = 0; i < k*MAX_N*2; ++i)
sums[i] = 0;
int n = 1;
while (1)
{
available_bins = 0;
for (i = 0; i < k; ++i)
if (!sums[i][n])
{
selection[available_bins] = i;
++available_bins;
}

if (!available_bins) break;

int bin = selection[rand() % available_bins];

bins[bin][sizes[bin]] = n;
++sizes[bin];
for (i = 0; i < sizes[bin]; ++i)
sums[bin][bins[bin][i] + n] = 1;

++n;
}

if (n > best)
{
best = n;
for (i = 0; i < k; ++i)
{
for (j = 0; j < sizes[i]; ++j)
printf("%d ", bins[i][j]);
printf("\n");
}
printf("%u\n", (unsigned)time(NULL));
}
}

return 0;
}

• Confirmed n=91, found in 138 seconds. If necessary for tie-breaking I'll retime to avoid large errors due to different CPU load. – Peter Taylor Nov 9 '15 at 22:49

# C++, 135

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <cmath>
#include <set>
#include <vector>
#include <algorithm>

using namespace std;

vector<vector<int> > subset;
vector<int> len, tmp;
set<int> sums;

bool is_sum_free_with(int elem, int subnr) {
sums.clear();
sums.insert(elem+elem);
for(int i=0; i<len[subnr]; ++i) {
sums.insert(subset[subnr][i]+elem);
for(int j=i; j<len[subnr]; ++j) sums.insert(subset[subnr][i]+subset[subnr][j]);
}
if(sums.find(elem)!=sums.end()) return false;
for(int i=0; i<len[subnr]; ++i) if(sums.find(subset[subnr][i])!=sums.end()) return false;
return true;
}

int main()
{
int k = 0; cin >> k;

int start=time(0);
cout << start << endl;

int allmax=0, cnt=0;
srand(0);

do {
len.clear();
len.resize(k);
subset.clear();
subset.resize(k);
for(int i=0; i<k; ++i) subset[i].resize((int)pow(3, k));

int n=0, last=0, c, y, g, h, t, max=0;
vector<int> p(k);

do {
++n;
c=-1;
for(int i=0; i++<k; ) {
y=(last+i)%k;
if(is_sum_free_with(n, y)) p[++c]=y;
}

if(c<0) --n;

t=n;

while(c<0) {
g=rand()%k;
h=rand()%len[g];
t=subset[g][h];
for(int l=h; l<len[g]-1; ++l) subset[g][l]=subset[g][l+1];
--len[g];
for(int i=0; i++<k; ) {
y=(g+i)%k;
if(is_sum_free_with(t, y) && y!=g) p[++c]=y;
}
if(c<0) subset[g][len[g]++]=t;
}

c=p[rand()%(c+1)];
subset[c][len[c]++]=t;

last=c;

if(n>max) {
max=n;
cnt=0;
if(n>allmax) {
allmax=n;
for(int i=0; i<k; ++i) {
tmp.clear();
for(int j=0; j<len[i]; ++j) tmp.push_back(subset[i][j]);
sort(tmp.begin(), tmp.end());
for(int j=0; j<len[i]; ++j) cout << tmp[j] << " ";
cout << endl;
}
cout << time(0) << " " << time(0)-start << " " << allmax << endl;
}

}

} while(++cnt<50*n && time(0)-start<600);

cnt=0;

} while(time(0)-start<600);

return 0;
}


Appends the next n to a randomly chosen subset. If that's not possible it removes random numbers from subsets and appends them to others in the hope that that enables appending n somewhere.

I prototyped this in awk, and because it looked promising, I translated it into C++ to speed it up. Using a std::set should even speed it up more.

### Output for n=135 (after around 230 seconds on my [old] machine)

2 6 9 10 13 17 24 28 31 35 39 42 43 46 50 57 61 68 75 79 90 94 97 101 105 108 119 123 126 127 130 131 134
38 41 45 48 51 52 55 56 58 59 62 64 65 66 67 69 70 71 72 74 78 80 81 84 85 87 88 91 95 98
5 12 15 16 19 22 23 25 26 29 33 36 73 83 93 100 103 107 110 111 113 114 117 120 121 124
1 4 11 14 21 27 34 37 40 47 53 60 76 86 89 96 99 102 109 112 115 122 125 132 135
3 7 8 18 20 30 32 44 49 54 63 77 82 92 104 106 116 118 128 129 133


I didn't recheck the validity, but it should be alright.

## Python 3, random greedy, n = 61

Last output:

[5, 9, 13, 20, 24, 30, 32, 34, 42, 46, 49, 57, 61]
[8, 12, 14, 23, 25, 44, 45, 47, 54]
[2, 6, 7, 19, 22, 27, 35, 36, 39, 40, 52, 53, 56]
[3, 10, 15, 16, 17, 29, 37, 51, 55, 59, 60]
[1, 4, 11, 18, 21, 26, 28, 31, 33, 38, 41, 43, 48, 50, 58]


This uses effectively the same algorithm as Martin Büttner's, but I developed it independently.

There are k bins that have both the numbers in it so far and the numbers that can't go in it anymore. At each depth in the iteration (it's basically a depth-first search), the bin ordering is shuffled and the next number (nextN) is (sequentially) put into the bins that can take it and then goes one step deeper. If there are none, it returns, backing up one step.

from copy import deepcopy
from random import shuffle, seed
from time import clock, time
global maxN
maxN = 0
clock()
seed(0)

def search(k,nextN=1,sets=None):
global maxN
if clock() > 600: return

if nextN == 1: #first iteration
sets = []
for i in range(k):
sets.append([[],[]])

R = list(range(k))
shuffle(R)
for i in R:
if clock() > 600: return
if nextN not in sets[i]:
sets2 = deepcopy(sets)
sets2[i].append(nextN)
sets2[i].extend([nextN+j for j in sets2[i]])
nextN2 = nextN + 1

if nextN > maxN:
maxN = nextN
print("New maximum!",maxN)
for s in sets2: print(s)
print(time())
print()

search(k, nextN2, sets2)

search(5)


# Python, n = 31

import sys
k = int(sys.argv)

for i in range(k):
print ([2**i * (2*j + 1) for j in range(2**(k - i - 1))])


Ok, so it's obvisouly not a winner, but I felt it belonged here anyway. I've taken the liberty not to include timestamps, since it terminates instantaneously, and since it's not a real contender.

First, note that the sum of any two odd numbers is even, so we can dump all the odd numbers in the first block. Then, since all the remaining numbers are even, we can divide them by 2. Once again, we can throw all the resulting odd numbers in the second block (after re-multiplying them by 2), divide the remaining numbers by 2 (i.e., by 4 overall), throw the odd ones in the third block (after re-multiplying them by 4), and so on... Or, to put in words you guys understand, we put all the numbers whose least-significant set bit is the first bit in the first block, all the numbers whose least-significant set bit is the second bit in the second block, and so on...

For k blocks, we run into trouble once we reach n = 2k, since the least-significant set bit of n is
the (k + 1)-th bit, which doesn't correspond to any block. In other words, this scheme works up
to n = 2k - 1. So, while for k = 5 we only get a meager n = 31, this number grows exponentially with k. It also establishes that S(k) ≥ 2k - 1 (but we can actually find a better lower limit than that quite easily.)

For reference, here's the result for k = 5:

[1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31]
[2, 6, 10, 14, 18, 22, 26, 30]
[4, 12, 20, 28]
[8, 24]


• There's an easy way to squeeze out an extra one: move the top half of the odd numbers to any other category (since their sum is bound to be larger than any number already in that category) and add 2^k to the bottom half of the odd numbers. The same idea can probably be extended to get another lg k numbers, or maybe even another k. – Peter Taylor Nov 10 '15 at 19:29
• @PeterTaylor Yeah, I realised short after posting that this is actually pretty trivial. It's equivalent to doing , [2,3], [4,5,6,7], ..., which is probably simpler, just with reverse bit and block order. It's easy to see how this one can be extended. – Ell Nov 10 '15 at 22:02