Idea
I interpret the Burnside's lemma as: number of orbits = arithmetic average of the fixed points of acts in G on set X. With this in mind, I used the calculation of PET and get something more pythonic (mostly simpler):
Code
I didn't try that hard to shorten the code so I kept things like Polynomial
, __str__
to make it easier to play with. For possible a shorter version, replacing Monomial with list and Polynomial with dict can save 20~30 lines.
This is python2 version but with few dependencies. It's not that hard to update it to python3.
from fractions import gcd
from math import factorial
Polynomial = dict
def solution(w, h, s):
matrix_cycle_index = calc_matrix_cycle_index_no_factorial(w, h)
exp_coe_map = {}
for monomial, coefficient in matrix_cycle_index.items():
exp = monomial.total_exp()
exp_coe_map[exp] = exp_coe_map.get(exp, 0) + coefficient
ret = sum([exp_coe_map[exp] * s ** exp for exp in exp_coe_map])
ret = ret / factorial(w) / factorial(h)
return str(ret)
class Monomial(list):
def __init__(self, vars = list()):
super(Monomial, self).__init__(vars)
def mul_var(self, var_ind, exp=1):
while var_ind >= len(self):
self.append(0)
self[var_ind] += exp
return self
def mul_mono(self, mono):
m = Monomial()
for i in range(len(mono)):
for j in range(len(self)):
exp_inc = gcd(i + 1, j + 1)
k = (i + 1) * (j + 1) // exp_inc
m.mul_var(k - 1, exp_inc * mono[i] * self[j])
return m
def total_exp(self):
return sum(self)
def __str__(self):
return '*'.join(['a%d^%d' % (i + 1, self[i]) for i in range(len(self)) if self[i] > 0])
def __hash__(self):
# regard self as an integer of base len(self)
return sum([self[i] * len(self) ** i for i in range(len(self))])
def calc_matrix_cycle_index_no_factorial(w, h):
polynomial = Polynomial()
cycleIndexW = calc_cycle_index_z_sn_no_factorial(w)
cycleIndexH = calc_cycle_index_z_sn_no_factorial(h)
for entryW in cycleIndexW.items():
for entryH in cycleIndexH.items():
m = entryW[0].mul_mono(entryH[0])
coe = entryW[1] * entryH[1]
polynomial[m] = polynomial.get(m, 0) + coe
return polynomial
cycle_indices = [Polynomial({Monomial(): 1})]
def calc_cycle_index_z_sn_no_factorial(setSize):
if len(cycle_indices) > setSize:
return cycle_indices[setSize]
factorial = 1
ret = Polynomial()
for k in range(setSize):
sub_cycle_index_no_factorial = calc_cycle_index_z_sn_no_factorial(setSize - (k + 1))
for mono, coe in sub_cycle_index_no_factorial.items():
mono = Monomial(mono).mul_var(k) # copy(mono)
ret[mono] = ret.get(mono,0) + factorial * coe
factorial *= setSize - (k + 1)
if len(cycle_indices) == setSize:
cycle_indices.append(ret)
return ret
Algorithm
Understanding the example
I first verified my algorithm with manual calculation:
- Take (w,h,s)=(2,3,4) as an example
- We know that the number of orbits is n=430, |G| = 322 =12. So n|G| = 12 * 430 = 5160. Let's verify $\Sigma_{g \in G} |S_g| = 5160$.
- Write out all elements in G: G = {e, c, 12, 12c, 23, 23c, 13, 13c, 123, 123c, 132, 132c}. |G| = 12. The
c
switches the two columns and numbers represents permutations of the lines of the matrix.
g |
why |
calc |
count |
cycle index |
e |
all x in S are fixed points |
4^6 |
4096 |
a1^6 |
c |
losing 3 degrees of freedoms |
4^3 |
64 |
a2^3 |
12 |
losing 2 degrees of freedoms |
4^4 |
256 |
a1^2 a2^2 |
12c |
losing 3 degrees of freedoms |
4^3 |
64 |
a3^2 |
23 |
losing 2 degrees of freedoms |
4^4 |
256 |
a1^2 a2^2 |
23c |
losing 3 degrees of freedoms |
4^3 |
64 |
a3^2 |
13 |
losing 2 degrees of freedoms |
4^4 |
256 |
a1^2 a2^2 |
13c |
losing 3 degrees of freedoms |
4^3 |
64 |
a3^2 |
123 |
only 2 degrees of freedoms left |
4^2 |
16 |
a2^3 |
123c |
only 1 degree of freedoms left |
4^1 |
4 |
a6 |
132 |
only 2 degrees of freedoms left |
4^2 |
16 |
a2^3 |
132c |
only 1 degree of freedoms left |
4^1 |
4 |
a6 |
"cycle index" of the table: see Burnside's Lemma applied to grids with interchanging rows and columns
The lost degrees of freedoms for a_m^n is n*(m-1)
The degrees of freedoms left is sum of the exponential
$6-(3-1)*2=2 = 6/3, 6-(2-1)*3=3, 6-(1-1)*6=6, 6-(6-1)*1=1, k-(k/n-1)*n=n, k- (k - n) = n$
And 4096 + 64 + 256 + 64 + 256 + 64 + 256 + 64 + 16 + 4 + 16 + 4 = 5160.
After this I found the "cycle index" way is much easier than my "counting lost degrees of freedom", although these above gave me a solid understanding of how to use PET & Burnside's Lemma.
Cycle Index
To calculate the "cycle index", I modified the last equation/formula of this wiki, and implemented it with the last function in my code, calc_cycle_index_z_sn_no_factorial
. But if you are allowed to use sympy, you can do something like below to get complete exponential Bell polynomial.
def ZNF(n):
return sum([
bell(n, k, [factorial(m-1) * Symbol("a"+str(m)) for m in range(1,n+1) ] )
for k in range(n+1)]) # bell from package `sympy`
And this would save the trouble of writing calc_cycle_index_z_sn_no_factorial
by yourself.
The purpose was to avoid Fraction()
but this also saved the trouble of gen_vars
and coeff
. In @ Koky Puebla 's code.
Conclusion
This is a combinatorics / enumeration problem. I spent quite some time reviewing my algebra. And now I feel more confident about the Burnside's lemma.
Appendix (TL;DR)
Test code for python
def test():
def assertEq(actual, expected):
assert expected == actual, (
"Expected " + str(expected) + " but got " + str(actual)
)
assertEq(solution(2, 2, 2), "7")
assertEq(solution(2, 3, 4), "430")
threeSquare = ["1", "36", "738", "8240", "57675", "289716", "1144836", "3780288"]
for i in range(len(threeSquare)):
assertEq(solution(3, 3, i + 1), threeSquare[i]) # https://oeis.org/A058001
fourSquare = [
"1",
"317",
"90492",
"7880456",
"270656150",
"4947097821",
"58002778967",
"490172624992",
]
for i in range(len(fourSquare)):
assertEq(solution(4, 4, i + 1), fourSquare[i]) # https://oeis.org/A058002
fiveSquare = ["1", "5624", "64796982", "79846389608", "20834113243925"]
for i in range(len(fiveSquare)):
assertEq(solution(5, 5, i + 1), fiveSquare[i]) # from https://oeis.org/A058003
sixSquare = ["1", "251610", "302752867740"]
for i in range(len(sixSquare)):
assertEq(solution(6, 6, i + 1), sixSquare[i]) # https://oeis.org/A058004
binaryMatrices = ["1", "2", "7", "36", "317", "5624", "251610", "33642660"]
for i in range(len(binaryMatrices)):
assertEq(solution(i, i, 2), binaryMatrices[i]) # https://oeis.org/A058005
assertEq(solution(2, 5, 4), "7882")
assertEq(solution(3, 5, 4), "1757384")
print("All tests passed")def test():
def assertEq(actual, expected):
assert expected == actual, (
"Expected " + str(expected) + " but got " + str(actual)
)
assertEq(solution(2, 2, 2), "7")
assertEq(solution(2, 3, 4), "430")
threeSquare = ["1", "36", "738", "8240", "57675", "289716", "1144836", "3780288"]
for i in range(len(threeSquare)):
assertEq(solution(3, 3, i + 1), threeSquare[i]) # https://oeis.org/A058001
fourSquare = [
"1",
"317",
"90492",
"7880456",
"270656150",
"4947097821",
"58002778967",
"490172624992",
]
for i in range(len(fourSquare)):
assertEq(solution(4, 4, i + 1), fourSquare[i]) # https://oeis.org/A058002
fiveSquare = ["1", "5624", "64796982", "79846389608", "20834113243925"]
for i in range(len(fiveSquare)):
assertEq(solution(5, 5, i + 1), fiveSquare[i]) # from https://oeis.org/A058003
sixSquare = ["1", "251610", "302752867740"]
for i in range(len(sixSquare)):
assertEq(solution(6, 6, i + 1), sixSquare[i]) # https://oeis.org/A058004
binaryMatrices = ["1", "2", "7", "36", "317", "5624", "251610", "33642660"]
for i in range(len(binaryMatrices)):
assertEq(solution(i, i, 2), binaryMatrices[i]) # https://oeis.org/A058005
assertEq(solution(2, 5, 4), "7882")
assertEq(solution(3, 5, 4), "1757384")
print("All tests passed")
Java code (same algorithm)
I wrote Java8 first, but not being able to use Math.BigInteger
for some reason (with google), same as for python2.7 and no sympy. So I had to switch to Python. I also adhere the full commented code in Java8, it passes all tests after changing long
in solution to (java.Math.)BigInteger
and it uses the same algorithm as Python above.
public class Solution {
public static void main(String[] args) {
long startTime = System.nanoTime();
// assertEq(solution(5, 5, 6), "1979525296377132");
assertEq(solution(2, 2, 2), "7");
assertEq(solution(2, 3, 4), "430");
String[] threeSquare = { "1", "36", "738", "8240", "57675", "289716", "1144836", "3780288" };
for (int i = 0; i < threeSquare.length; i++) {
assertEq(solution(3, 3, i + 1), threeSquare[i]);
} // https://oeis.org/A058001
String[] fourSquare = { "1", "317", "90492", "7880456", "270656150", "4947097821", "58002778967",
"490172624992" };
for (int i = 0; i < fourSquare.length; i++) {
assertEq(solution(4, 4, i + 1), fourSquare[i]);
} // https://oeis.org/A058002
String[] fiveSquare = { "1", "5624", "64796982",
"79846389608", "20834113243925" };
// cannot test with long , "1979525296377132", "93242242505023122"
for (int i = 0; i < fiveSquare.length; i++) {
assertEq(solution(5, 5, i + 1), fiveSquare[i]);
} // from https://oeis.org/A058003
String[] sixSquare = { "1", "251610", "302752867740" }; // , "9178323524804624", "28125393244553141210" };
for (int i = 0; i < sixSquare.length; i++) {
assertEq(solution(6, 6, i + 1), sixSquare[i]);
} // https://oeis.org/A058004
String[] binaryMatrices = {
"1", "2", "7", "36", "317", "5624", "251610", "33642660" };
// , "14685630688", "21467043671008","105735224248507784",};
for (int i = 0; i < binaryMatrices.length; i++) {
assertEq(solution(i, i, 2), binaryMatrices[i]);
} // https://oeis.org/A058005
// I made these up
assertEq(solution(2, 5, 4), "7882");
assertEq(solution(3, 5, 4), "1757384");
long endTime = System.nanoTime();
long duration = (endTime - startTime);
System.out.println("Time: " + duration);
}
/**
* Solution of this problem: The count of orbits of the M_{w,h} under group
* action of S_w x S_h
*
* @param w - width of the grid, 1 <= w <= 12
* @param h - height of the grid, 1 <= h <= 12
* @param s - number of states of each celestial body, 2 <= s <= 20
* @return
*/
public static String solution(int w, int h, int s) {
Polynomial matrixCycleIndex = calcMatrixCycleIndexNoFactorial(w, h);
long ret = 0;
HashMap<Integer, Integer> expCoeMap = new HashMap<>();
long minExp = Long.MAX_VALUE;
for (Map.Entry<Monomial, Integer> entry : matrixCycleIndex.entrySet()) {
int exp = entry.getKey().totalExp();
minExp = Math.min(minExp, exp);
expCoeMap.put(exp, expCoeMap.getOrDefault(exp, 0) + entry.getValue());
}
for (Map.Entry<Integer, Integer> entry : expCoeMap.entrySet()) {
long fixedPointNum = entry.getValue() * longPower(s, entry.getKey());
ret = ret + fixedPointNum;
}
for (int factorialLimit : Arrays.asList(w, h)) {
for (int i = 2; i <= factorialLimit; i++) {
ret /= i;
}
}
return String.valueOf(ret);
}
public static class Monomial { // or extends ArrayList<Integer>
ArrayList<Integer> vars;
public Monomial(List<Integer> vars) {
this.vars = new ArrayList<>(vars);
}
public Monomial(Monomial m) {
this.vars = new ArrayList<>(m.vars);
}
public Monomial mulVar(int varInd, int exp) {
while (varInd >= vars.size()) {
vars.add(0);
}
vars.set(varInd, vars.get(varInd) + exp);
return this;
}
public Monomial mulVar(int varInd) {
return mulVar(varInd, 1);
}
/**
* Multiply monomial by a special rule.
* Variable a_m * a_n = a_{lcm(m,n)} ^ gcd(m,n)
*
* Test:
* assertEq(new Mono(Arrays.asList(0, 1))
* .mulMono(new Mono(Arrays.asList(1, 1))),
* new Mono(Arrays.asList(0, 3)));
* assertEq(new Mono(Arrays.asList(2))
* .mulMono(new Mono(Arrays.asList(3))),
* new Mono(Arrays.asList(6)));
*
* @param mono
* @return
*/
public Monomial mulMono(Monomial mono) {
Monomial m = new Monomial(new ArrayList<>());
for (int i = 0; i < mono.vars.size(); i++) {
for (int j = 0; j < this.vars.size(); j++) {
int k = lcmOf(i + 1, j + 1); // to 1-based
int expInc = gcdOf(i + 1, j + 1); // to 1-based
m.mulVar(k - 1, expInc * mono.vars.get(i) * this.vars.get(j)); // TODO: ref: use 1 based
}
}
return m;
}
public int totalExp() {
return this.vars.stream().mapToInt(Integer::intValue).sum();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < this.vars.size(); i++) {
if (vars.get(i) > 0) {
sb.append("a");
sb.append(i + 1); // using 1-based index
if (vars.get(i) > 1) {
sb.append("^");
sb.append(vars.get(i));
}
}
}
return sb.toString();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Monomial) {
Monomial m = (Monomial) obj;
return this.vars.equals(m.vars); // or use hashCode
}
return false;
}
@Override
public int hashCode() {
int id = 0;
int len = vars.size();
for (int i = 0; i < len; i++) {
id += vars.get(i) * Math.pow(len, i);
}
return id;
}
}
public static class Polynomial extends HashMap<Monomial, Integer> {
}
public static Polynomial calcMatrixCycleIndexNoFactorial(int w, int h) {
Polynomial polynomial = new Polynomial();
Polynomial cycleIndexW = calcCycleIndexZSnNoFactorial(w);
Polynomial cycleIndexH = calcCycleIndexZSnNoFactorial(h);
for (Map.Entry<Monomial, Integer> entryW : cycleIndexW.entrySet()) {
for (Map.Entry<Monomial, Integer> entryH : cycleIndexH.entrySet()) {
Monomial m = entryW.getKey().mulMono(entryH.getKey());
int coe = entryW.getValue() * entryH.getValue();
polynomial.put(m, polynomial.getOrDefault(m, 0) + coe);
}
}
return polynomial;
}
static ArrayList<Polynomial> cycleIndices = new ArrayList<>();
/**
* Recursively calculates the partitions of Cycle Index without Factorial(n)
* Impl of https://en.wikipedia.org/wiki/Cycle_index#Symmetric_group_Sn
* The coefficients add up to Bell Number B_n
* This is not https://oeis.org/A178867
*
* Algorithm: (1-based index)
* ZNF(S_n) = Z(S_n) * (n!)
* Z(S_n) = 1/n \Sigma_{k=1}^n {a_k * Z(S_{n-k})}
* ZNF(S_n) = \Sigma_{k=1}^n { (n-1)! * a_k * ZNF(S_{n-k}) / (n-k)! }
* ZNF(S_n) = \Sigma_{k=1}^n { (n-1)! / (n-k)! * a_k * ZNF(S_{n-k}) }
*
* To verify: (on https://live.sympy.org/ use expand, should all be 0)
*
* def ZNF(n):
* return sum([
* bell(n, k, [factorial(m-1) * Symbol("a"+str(m)) for m in range(1,n+1) ] )
* for k in range(n+1)])
*
* expand(ZNF(3)-2*Symbol("a3")*ZNF(0)-2*Symbol("a2")*ZNF(1)-1*Symbol("a1")*ZNF(2))
* ZNF(4)-6*Symbol("a4")*ZNF(0)-6*Symbol("a3")*ZNF(1)-3*Symbol("a2")*ZNF(2)-1*Symbol("a1")*ZNF(3)
* expand(ZNF(5)-24*Symbol("a5")*ZNF(0)-24*Symbol("a4")*ZNF(1)-12*Symbol("a3")*ZNF(2)
* -4*Symbol("a2")*ZNF(3)-1*Symbol("a1")*ZNF(4))
*
* @param setSize - the number of elements in the set
* @return Poly, all monomials and their coefficients of the
* polynomial
*
* @example
* * ZNF(S_0) = 1
* * ZNF(S_1) = a1
* * ZNF(S_2) = a1^2 + a2
* * ZNF(S_3) = a1^3 + 3a1a2 + 2a3
* * ZNF(S_4) = a1^4 + 6a1^2a2 + 8a1a3 + 3a2^2 + 6a4
* * ZNF(S_5) = a1^5 + 10a1^3a2 + 20a1^2a3 + 15a1a2^2 + 30a1a4 + 20a2a3
* + 24a5
*
* @see https://live.sympy.org/
* A easier way is hard coding the 12 results of
* ```
* [
* sum([
* bell(n, k, [factorial(m-1) * Symbol("a"+str(m)) for m in range(1,n+1)])
* for k in range(n+1)]) # these 3 lines are same as ZNF(n)
* for n in range(1,13)]
* ```
* in SymPy
*/
public static Polynomial calcCycleIndexZSnNoFactorial(Integer setSize) {
if (cycleIndices.size() > setSize) {
return cycleIndices.get(setSize);
}
if (setSize == 0) {
Polynomial ret = new Polynomial();
ret.put(new Monomial(new ArrayList<>()), 1);
if (cycleIndices.isEmpty()) {
cycleIndices.add(ret);
}
return ret;
}
int factorial = 1;
Polynomial ret = new Polynomial();
for (int k = 0; k < setSize; k++) {
int eleLeft = (setSize - (k + 1));
Polynomial subCycleIndexNoFactorial = calcCycleIndexZSnNoFactorial(eleLeft);
for (Map.Entry<Monomial, Integer> entry : subCycleIndexNoFactorial.entrySet()) {
Monomial mono = new Monomial(entry.getKey());
mono.mulVar(k);
Integer oldCoe = ret.get(mono);
if (oldCoe == null) {
oldCoe = 0;
}
ret.put(mono, oldCoe + factorial * entry.getValue());
}
factorial *= (eleLeft);
}
if (cycleIndices.size() == setSize) {
cycleIndices.add(ret);
}
return ret;
}
public static int gcdOf(int m, int n) {
if (m == 0 || n == 0) {
return 0;
}
if (m == n) {
return m;
}
if (m > n) {
return gcdOf(m - n, n);
}
return gcdOf(m, n - m);
}
public static int lcmOf(int m, int n) {
if (m == 0 || n == 0) {
return 0;
}
return m * n / gcdOf(m, n);
}
public static long longPower(long base, long power) {
long ret = 1;
for (long i = 0; i < power; i++) {
ret *= base;
}
return ret;
}
public static <T> void assertEq(T actual, T expected) {
if (expected == actual || expected.equals(actual)) {
return;
}
throw new AssertionError("Expected " + expected.toString() + " but got " + actual.toString());
}
}
Python
In python I did some refactor to improve the readability but it slows down the python execution (from 0.015s to 0.024s on my machine running all the tests below). I think the implementation can still be improved.