Starting from permutations and combinations, I think we're interested in combinations:
For 30 topics, you would have the followng count of combinations for each size:
(In other words there are 30 combinations of size 1, and 435 combinations of size 2 (also 435 of size 28 as it turns out)).
I think you could generate each of the combinations for each size of combination from 1-30. You can google "generate all combinations", and, also, I have attached one that I whipped up below.
For each combination size, we might find the combinations, and find occurrences of them among students, and sort them by their count. You could have a cutoff of interest, keeping only the top some % for each size.
We can store each student's topic score with one bit. Thus, for 30 topics, we need 30 bits per student. On a 64-bit machine, we can store up to 64 bits in a single integer.
We can represent a combination also as an integer, whose bits are set to represent what elements are in the combination. In other words, a combination of size 6 is represented by an integer having exactly 6 of its bits set.
So, given a combination, we can test each of the ~200 students for match with a simple logical bitwise AND operation followed by a compare.
for ( var i = 0; i < 200; i++ )
if ( (studentScores [i] & currentCombinaton) == currentCombination )
// this combination matched for this student.
NOTE as written this is checking for success combinations not failure combinations, but that is easily reversed (using
== 0 instead of
So, that's a start.
I would think that you could do an analysis of combinations relative to each other, such as what popular combinations of size 4 or 5 are found in the popular combinations of size 16.
Below is a C# program I wrote that generates combinations of T/F values as bit mask. You could put that combo generator in a size loop, and use the above student occurrence loop for each such size.
static ulong NextCombination ( int outOf, ulong last )
for ( i = 0; i < outOf; i++ )
if ( (last & (1UL << i)) != 0 )
for ( j = i + 1; j < outOf; j++ )
if ( (last & (1UL << j)) == 0 )
// we alter the number by removing all the consecutive ones
// that occur followed by a zero (viewed from low order to high order)
// as in ...0111...
// replace with a single 1
// as in ...1000... and set low bits to bits in a row, i.e.
int numberOfOnesInARowBeforeFirstZero = j - i;
var onesMask = ((1UL << numberOfOnesInARowBeforeFirstZero) - 1) << i;
last &= ~onesMask; // clear the series of one's
last |= 1UL << j; // set the first zero bit after the ones in a row
var lowOnesReplacement = (1UL << (numberOfOnesInARowBeforeFirstZero - 1)) - 1;
last |= lowOnesReplacement; // set the lowest bits to make up for the cleared bits
static void Main ( string  args )
int comboSize = 3;
int outOfSize = 30;
int count = 1;
ulong currentCombination = (1UL << comboSize) - 1;
ulong lastCombination = currentCombination << (outOfSize - comboSize);
System.Console.WriteLine ( "Last Combination for size " + comboSize + " is " + Convert.ToString ( (long) lastCombination, 2 ).PadLeft ( outOfSize, '0' ) );
for ( ;; )
System.Console.WriteLine ( "Combination " + count + " is " + Convert.ToString ( (long) currentCombination, 2 ).PadLeft ( outOfSize, '0' ) );
if ( currentCombination == lastCombination )
currentCombination = NextCombination ( outOfSize, currentCombination );