latin-rectangles 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- latin_rectangles/__init__.py +40 -0
- latin_rectangles/__main__.py +186 -0
- latin_rectangles/derangements.py +107 -0
- latin_rectangles/extension_counting.py +49 -0
- latin_rectangles/py.typed +0 -0
- latin_rectangles/rook_polynomials.py +71 -0
- latin_rectangles-0.1.0.dist-info/METADATA +290 -0
- latin_rectangles-0.1.0.dist-info/RECORD +11 -0
- latin_rectangles-0.1.0.dist-info/WHEEL +4 -0
- latin_rectangles-0.1.0.dist-info/entry_points.txt +2 -0
- latin_rectangles-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Latin rectangles extension counting algorithms."""
|
|
2
|
+
|
|
3
|
+
from .derangements import find_cycle_decomposition, generate_random_derangement
|
|
4
|
+
from .extension_counting import count_extensions
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def count_random_extensions(n: int) -> int:
|
|
8
|
+
"""
|
|
9
|
+
Generate a random derangement of size n and count its extensions.
|
|
10
|
+
|
|
11
|
+
This is a convenience function that combines random derangement generation
|
|
12
|
+
with extension counting in a single call.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
n: Size of the derangement (must be > 1)
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Number of extensions for the randomly generated derangement
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
ValueError: If n <= 1 (no derangements exist)
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
>>> extensions = count_random_extensions(10)
|
|
25
|
+
>>> print(f"Random derangement for n=10 has {extensions} extensions")
|
|
26
|
+
"""
|
|
27
|
+
if n <= 1:
|
|
28
|
+
raise ValueError("n must be greater than 1 for derangements to exist")
|
|
29
|
+
|
|
30
|
+
random_derangement = generate_random_derangement(n)
|
|
31
|
+
return count_extensions(random_derangement)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Export the main functions
|
|
35
|
+
__all__ = [
|
|
36
|
+
"count_extensions",
|
|
37
|
+
"count_random_extensions",
|
|
38
|
+
"find_cycle_decomposition",
|
|
39
|
+
"generate_random_derangement",
|
|
40
|
+
]
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Entry point for running the latin_rectangles package as a script."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from .derangements import find_cycle_decomposition, generate_random_derangement, create_cycle_structure
|
|
7
|
+
from .extension_counting import count_extensions
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def count_random_extensions(n: int) -> tuple[int, list[int], int]:
|
|
11
|
+
"""
|
|
12
|
+
Generate a random derangement and count its extensions.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
n: Size of the derangement
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Tuple of (n, cycle_lengths, extensions_count)
|
|
19
|
+
"""
|
|
20
|
+
if n <= 1:
|
|
21
|
+
raise ValueError("n must be greater than 1 for derangements to exist")
|
|
22
|
+
|
|
23
|
+
random_p = generate_random_derangement(n)
|
|
24
|
+
random_cycles = find_cycle_decomposition(random_p)
|
|
25
|
+
cycle_lengths = sorted([len(c) for c in random_cycles])
|
|
26
|
+
extensions = count_extensions(random_p)
|
|
27
|
+
|
|
28
|
+
return n, cycle_lengths, extensions
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def count_cycle_structure_extensions(cycle_structure: str) -> tuple[int, list[int], int]:
|
|
32
|
+
"""
|
|
33
|
+
Create a derangement with specific cycle structure and count its extensions.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
cycle_structure: Comma-separated cycle lengths (e.g., "2,2,4")
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Tuple of (n, cycle_lengths, extensions_count)
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
cycle_lengths = [int(x.strip()) for x in cycle_structure.split(",")]
|
|
43
|
+
except ValueError:
|
|
44
|
+
raise ValueError("Cycle structure must be comma-separated integers")
|
|
45
|
+
|
|
46
|
+
if not cycle_lengths:
|
|
47
|
+
raise ValueError("Cycle structure cannot be empty")
|
|
48
|
+
|
|
49
|
+
n = sum(cycle_lengths)
|
|
50
|
+
if n <= 1:
|
|
51
|
+
raise ValueError("Total size must be greater than 1")
|
|
52
|
+
|
|
53
|
+
p = create_cycle_structure(cycle_lengths)
|
|
54
|
+
extensions = count_extensions(p)
|
|
55
|
+
|
|
56
|
+
return n, sorted(cycle_lengths), extensions
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def generate_all_cycle_structures(n: int) -> list[list[int]]:
|
|
60
|
+
"""
|
|
61
|
+
Generate all valid cycle structures (partitions) for a derangement of size n.
|
|
62
|
+
Only includes partitions where all parts are ≥ 2 (no 1-cycles).
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
n: Size of the derangement
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
List of cycle structures, each as a sorted list of cycle lengths
|
|
69
|
+
"""
|
|
70
|
+
def partitions_with_min_part(target: int, min_part: int, current: list[int]) -> list[list[int]]:
|
|
71
|
+
"""Generate partitions of target where all parts are >= min_part."""
|
|
72
|
+
if target == 0:
|
|
73
|
+
return [current[:]]
|
|
74
|
+
|
|
75
|
+
if target < min_part:
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
result = []
|
|
79
|
+
for part_size in range(min_part, target + 1):
|
|
80
|
+
current.append(part_size)
|
|
81
|
+
result.extend(partitions_with_min_part(target - part_size, part_size, current))
|
|
82
|
+
current.pop()
|
|
83
|
+
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
if n <= 1:
|
|
87
|
+
return []
|
|
88
|
+
|
|
89
|
+
# Generate all partitions where each part is at least 2
|
|
90
|
+
partitions = partitions_with_min_part(n, 2, [])
|
|
91
|
+
|
|
92
|
+
# Sort each partition for consistent output
|
|
93
|
+
return [sorted(partition) for partition in partitions]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def enumerate_all_extensions(n: int) -> list[tuple[list[int], int]]:
|
|
97
|
+
"""
|
|
98
|
+
Enumerate all possible cycle structures for n and count their extensions.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
n: Size of the derangement
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
List of tuples (cycle_structure, extensions_count) sorted by extensions_count
|
|
105
|
+
"""
|
|
106
|
+
structures = generate_all_cycle_structures(n)
|
|
107
|
+
results = []
|
|
108
|
+
|
|
109
|
+
for cycle_lengths in structures:
|
|
110
|
+
p = create_cycle_structure(cycle_lengths)
|
|
111
|
+
extensions = count_extensions(p)
|
|
112
|
+
results.append((cycle_lengths, extensions))
|
|
113
|
+
|
|
114
|
+
# Sort by extensions count (descending), then by cycle structure
|
|
115
|
+
results.sort(key=lambda x: (-x[1], x[0]))
|
|
116
|
+
return results
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def main() -> None:
|
|
120
|
+
"""Main function with command-line interface for Latin rectangle extension counting."""
|
|
121
|
+
parser = argparse.ArgumentParser(
|
|
122
|
+
description="Latin Rectangles Extension Counter",
|
|
123
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
124
|
+
epilog="""
|
|
125
|
+
Examples:
|
|
126
|
+
%(prog)s --n 42 # Generate random derangement for n=42
|
|
127
|
+
%(prog)s --c "2,2,4" # Use specific cycle structure: two 2-cycles and one 4-cycle
|
|
128
|
+
%(prog)s --c "8" # Single 8-cycle
|
|
129
|
+
%(prog)s --c "2,2,2,2" # Four 2-cycles
|
|
130
|
+
%(prog)s --n 8 --all # Enumerate all possible cycle structures for n=8
|
|
131
|
+
""",
|
|
132
|
+
)
|
|
133
|
+
# Add --n option for backward compatibility
|
|
134
|
+
parser.add_argument("--n", type=int, help="Size of the derangement (must be > 1)")
|
|
135
|
+
parser.add_argument("--c", type=str, help="Cycle structure as comma-separated integers (e.g., '2,2,4' for two 2-cycles and one 4-cycle)")
|
|
136
|
+
parser.add_argument("--all", action="store_true", help="Enumerate all possible cycle structures for given n (use with --n)")
|
|
137
|
+
|
|
138
|
+
args = parser.parse_args()
|
|
139
|
+
|
|
140
|
+
if args.n and args.c:
|
|
141
|
+
print("❌ Error: Cannot specify both --n and --c arguments", file=sys.stderr)
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
|
|
144
|
+
if args.c and args.all:
|
|
145
|
+
print("❌ Error: Cannot use --all with --c (use --all with --n)", file=sys.stderr)
|
|
146
|
+
sys.exit(1)
|
|
147
|
+
|
|
148
|
+
if not args.n and not args.c:
|
|
149
|
+
parser.print_help()
|
|
150
|
+
sys.exit(1)
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
if args.n and args.all:
|
|
154
|
+
# Enumerate all cycle structures mode
|
|
155
|
+
results = enumerate_all_extensions(args.n)
|
|
156
|
+
if not results:
|
|
157
|
+
print(f"❌ No valid cycle structures found for n={args.n}")
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
|
|
160
|
+
print(f"🔍 All Cycle Structures for n={args.n}")
|
|
161
|
+
print(f"📊 Found {len(results)} possible structures with non-zero extensions:")
|
|
162
|
+
print()
|
|
163
|
+
|
|
164
|
+
for i, (cycle_structure, extensions) in enumerate(results, 1):
|
|
165
|
+
if extensions > 0: # Only show structures with non-zero extensions
|
|
166
|
+
print(f"{i:2d}. {cycle_structure} → {extensions:,} extensions")
|
|
167
|
+
|
|
168
|
+
elif args.n:
|
|
169
|
+
# Generate random derangement mode
|
|
170
|
+
n_val, cycle_lengths, extensions = count_random_extensions(args.n)
|
|
171
|
+
print(f"🎲 Generated Random Derangement for n={n_val}")
|
|
172
|
+
print(f"📊 Cycle structure: {cycle_lengths}")
|
|
173
|
+
print(f"🔢 Number of extensions: {extensions:,}")
|
|
174
|
+
elif args.c:
|
|
175
|
+
# Specific cycle structure mode
|
|
176
|
+
n_val, cycle_lengths, extensions = count_cycle_structure_extensions(args.c)
|
|
177
|
+
print(f"⚙️ Specific Cycle Structure for n={n_val}")
|
|
178
|
+
print(f"📊 Cycle structure: {cycle_lengths}")
|
|
179
|
+
print(f"🔢 Number of extensions: {extensions:,}")
|
|
180
|
+
except ValueError as e:
|
|
181
|
+
print(f"❌ Error: {e}", file=sys.stderr)
|
|
182
|
+
sys.exit(1)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if __name__ == "__main__":
|
|
186
|
+
main()
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Functions for generating and working with derangements."""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate_random_derangement(n: int) -> list[int]:
|
|
7
|
+
"""
|
|
8
|
+
Quickly generates a random derangement of length n.
|
|
9
|
+
A derangement is a permutation p of {1, ..., n} such that p[i] != i.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
n: The size of the derangement.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
A list of length n+1 representing the derangement (1-indexed).
|
|
16
|
+
|
|
17
|
+
Raises:
|
|
18
|
+
ValueError: If n=1 (no derangements exist) or n < 0.
|
|
19
|
+
"""
|
|
20
|
+
if n == 1:
|
|
21
|
+
raise ValueError("No derangements exist for n=1.")
|
|
22
|
+
if n < 0:
|
|
23
|
+
raise ValueError("n must be non-negative.")
|
|
24
|
+
if n == 0:
|
|
25
|
+
return [0]
|
|
26
|
+
|
|
27
|
+
while True:
|
|
28
|
+
# Create a list of numbers from 1 to n
|
|
29
|
+
p = list(range(1, n + 1))
|
|
30
|
+
# Shuffle the list to get a random permutation
|
|
31
|
+
random.shuffle(p)
|
|
32
|
+
|
|
33
|
+
# Check if it's a derangement (p[i] != i+1 for 0-indexed list)
|
|
34
|
+
is_derangement = True
|
|
35
|
+
for i in range(n):
|
|
36
|
+
if p[i] == i + 1:
|
|
37
|
+
is_derangement = False
|
|
38
|
+
break
|
|
39
|
+
|
|
40
|
+
if is_derangement:
|
|
41
|
+
# Prepend a 0 for 1-based indexing and return
|
|
42
|
+
return [0, *p]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def find_cycle_decomposition(p: list[int]) -> list[list[int]]:
|
|
46
|
+
"""
|
|
47
|
+
Finds the cycle decomposition of a permutation.
|
|
48
|
+
Permutation p is 1-indexed, so p[0] is ignored.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
p: 1-indexed permutation where p[0] is ignored.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
List of cycles, where each cycle is represented as a list of indices.
|
|
55
|
+
"""
|
|
56
|
+
n = len(p) - 1
|
|
57
|
+
visited = [False] * (n + 1)
|
|
58
|
+
cycles = []
|
|
59
|
+
for i in range(1, n + 1):
|
|
60
|
+
if not visited[i]:
|
|
61
|
+
current_cycle = []
|
|
62
|
+
j = i
|
|
63
|
+
while not visited[j]:
|
|
64
|
+
visited[j] = True
|
|
65
|
+
current_cycle.append(j)
|
|
66
|
+
j = p[j]
|
|
67
|
+
cycles.append(current_cycle)
|
|
68
|
+
return cycles
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def create_cycle_structure(cycle_lengths: list[int]) -> list[int]:
|
|
72
|
+
"""
|
|
73
|
+
Create a derangement with a specific cycle structure.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
cycle_lengths: List of desired cycle lengths
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
1-indexed permutation with the specified cycle structure
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
ValueError: If cycle_lengths contains a 1-cycle (would create fixed point)
|
|
83
|
+
"""
|
|
84
|
+
if 1 in cycle_lengths:
|
|
85
|
+
raise ValueError("Cycle structure cannot contain 1-cycles (would create fixed points)")
|
|
86
|
+
|
|
87
|
+
n = sum(cycle_lengths)
|
|
88
|
+
if n == 0:
|
|
89
|
+
return [0]
|
|
90
|
+
|
|
91
|
+
perm = [0] * (n + 1) # 1-indexed with 0 at start
|
|
92
|
+
current_pos = 1
|
|
93
|
+
|
|
94
|
+
for cycle_len in cycle_lengths:
|
|
95
|
+
# Get positions for this cycle
|
|
96
|
+
cycle_positions = list(range(current_pos, current_pos + cycle_len))
|
|
97
|
+
|
|
98
|
+
# Create the cycle: each position points to the next, last points to first
|
|
99
|
+
for i in range(cycle_len):
|
|
100
|
+
perm[cycle_positions[i]] = cycle_positions[(i + 1) % cycle_len]
|
|
101
|
+
|
|
102
|
+
current_pos += cycle_len
|
|
103
|
+
|
|
104
|
+
return perm
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
__all__ = ["find_cycle_decomposition", "generate_random_derangement", "create_cycle_structure"]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Main extension counting algorithm for Latin rectangles."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
from .derangements import find_cycle_decomposition
|
|
6
|
+
from .rook_polynomials import get_rook_polynomial_for_cycle, multiply_polynomials
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def count_extensions(permutation: list[int]) -> int:
|
|
10
|
+
"""
|
|
11
|
+
Calculates the number of ways to extend a 2xn Latin rectangle to a 3xn one.
|
|
12
|
+
This is the most robust and general implementation.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
permutation: A list representing the second row, assuming the first row is
|
|
16
|
+
(1, 2, ..., n). The list should be 1-indexed, so its
|
|
17
|
+
length is n+1 and permutation[0] can be a dummy value.
|
|
18
|
+
It must be a derangement.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
The integer number of possible third rows.
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
ValueError: If the input permutation is not a derangement.
|
|
25
|
+
"""
|
|
26
|
+
n = len(permutation) - 1
|
|
27
|
+
if any(i == val for i, val in enumerate(permutation[1:], 1)):
|
|
28
|
+
raise ValueError("Input permutation must be a derangement (p(i) != i).")
|
|
29
|
+
|
|
30
|
+
# 1. Find the cycle decomposition of the permutation
|
|
31
|
+
cycles = find_cycle_decomposition(permutation)
|
|
32
|
+
|
|
33
|
+
# 2. Get the total rook polynomial by multiplying the polynomials of the sub-problems
|
|
34
|
+
total_rook_poly = [1] # Start with the polynomial "1"
|
|
35
|
+
for cycle in cycles:
|
|
36
|
+
k = len(cycle)
|
|
37
|
+
cycle_rook_poly = get_rook_polynomial_for_cycle(k)
|
|
38
|
+
total_rook_poly = multiply_polynomials(total_rook_poly, cycle_rook_poly)
|
|
39
|
+
|
|
40
|
+
# 3. Apply the Principle of Inclusion-Exclusion to get the final count.
|
|
41
|
+
total_ways = 0
|
|
42
|
+
for k, h_k in enumerate(total_rook_poly):
|
|
43
|
+
term = ((-1) ** k) * h_k * math.factorial(n - k)
|
|
44
|
+
total_ways += term
|
|
45
|
+
|
|
46
|
+
return total_ways
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
__all__ = ["count_extensions"]
|
|
File without changes
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Rook polynomial calculations for Latin rectangles."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
# Memoization cache for rook polynomials to avoid re-computation
|
|
6
|
+
_ROOK_POLY_CACHE: dict[int, list[int]] = {}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_rook_polynomial_for_cycle(k: int) -> list[int]:
|
|
10
|
+
"""
|
|
11
|
+
Calculates the rook polynomial for the forbidden board of a k-cycle.
|
|
12
|
+
The formula for the j-th coefficient is taken from the Menage problem:
|
|
13
|
+
r_j(k) = (2k / (2k - j)) * C(2k - j, j)
|
|
14
|
+
where C is the binomial coefficient "n-choose-k".
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
k: The cycle length.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
List of coefficients for the rook polynomial.
|
|
21
|
+
"""
|
|
22
|
+
if k in _ROOK_POLY_CACHE:
|
|
23
|
+
return _ROOK_POLY_CACHE[k]
|
|
24
|
+
|
|
25
|
+
# The rook polynomial has degree k, so it has k+1 coefficients.
|
|
26
|
+
coeffs = [0] * (k + 1)
|
|
27
|
+
|
|
28
|
+
# r_0 is always 1
|
|
29
|
+
coeffs[0] = 1
|
|
30
|
+
|
|
31
|
+
for j in range(1, k + 1):
|
|
32
|
+
# This handles the case j=2k, where the denominator would be zero.
|
|
33
|
+
# In that situation, the binomial coefficient C(0, 2k) is 0 anyway.
|
|
34
|
+
if (2 * k - j) < j:
|
|
35
|
+
# C(n, k) is 0 if k > n
|
|
36
|
+
coeffs[j] = 0
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
numerator = 2 * k
|
|
40
|
+
denominator = 2 * k - j
|
|
41
|
+
|
|
42
|
+
# We use integer division `//` as the result is always an integer.
|
|
43
|
+
# This keeps calculations exact and avoids floating point issues.
|
|
44
|
+
term1 = (numerator * math.comb(denominator, j)) // denominator
|
|
45
|
+
coeffs[j] = term1
|
|
46
|
+
|
|
47
|
+
_ROOK_POLY_CACHE[k] = coeffs
|
|
48
|
+
return coeffs
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def multiply_polynomials(poly1: list[int], poly2: list[int]) -> list[int]:
|
|
52
|
+
"""
|
|
53
|
+
Multiplies two polynomials given as lists of coefficients.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
poly1: First polynomial as list of coefficients.
|
|
57
|
+
poly2: Second polynomial as list of coefficients.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Product polynomial as list of coefficients.
|
|
61
|
+
"""
|
|
62
|
+
len1, len2 = len(poly1), len(poly2)
|
|
63
|
+
new_len = len1 + len2 - 1
|
|
64
|
+
result_poly = [0] * new_len
|
|
65
|
+
for i in range(len1):
|
|
66
|
+
for j in range(len2):
|
|
67
|
+
result_poly[i + j] += poly1[i] * poly2[j]
|
|
68
|
+
return result_poly
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
__all__ = ["get_rook_polynomial_for_cycle", "multiply_polynomials"]
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: latin-rectangles
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Count the number of one-row extensions of Latin rectangles.
|
|
5
|
+
Author-email: Ioannis Michaloliakos <ioannis.michalol@ufl.edu>
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# Latin Rectangles Extension Counter
|
|
11
|
+
|
|
12
|
+
A high-performance Python library for counting the number of ways to extend a 2×n [Latin rectangle](https://en.wikipedia.org/wiki/Latin_rectangle) to a 3×n Latin rectangle using rook polynomial methods and cycle decomposition theory.
|
|
13
|
+
|
|
14
|
+
[](https://www.python.org/downloads/)
|
|
15
|
+
[](LICENSE)
|
|
16
|
+
|
|
17
|
+
## Overview
|
|
18
|
+
|
|
19
|
+
A **Latin rectangle** is an r×n array filled with n different symbols such that each symbol occurs exactly once in each row and at most once in each column.
|
|
20
|
+
|
|
21
|
+
### Extension Problem
|
|
22
|
+
|
|
23
|
+
Given a 2×n Latin rectangle:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
1 2 3 4 5 6 7 8
|
|
27
|
+
p[1] p[2] p[3] p[4] p[5] p[6] p[7] p[8]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
where `p` is a [derangement](https://en.wikipedia.org/wiki/Derangement), the problem is to count how many valid third rows can be added such that the resulting 3×n rectangle remains a Latin rectangle. This library provides an efficient algorithm for computing Latin rectangle extensions.
|
|
31
|
+
|
|
32
|
+
### Key Features
|
|
33
|
+
|
|
34
|
+
- **High Performance**: Approximate O(n^2) time complexity, tested **up to n=800**.
|
|
35
|
+
- **Memory Efficient**: Approximate O(n^1.36) memory complexity
|
|
36
|
+
- **Mathematically Rigorous**: Based on rook polynomial theory and cycle decomposition
|
|
37
|
+
- **Easy to Use**: Simple command-line interface and Python API
|
|
38
|
+
- **Well Tested**: Comprehensive test suite with complexity analysis
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```console
|
|
43
|
+
git clone https://github.com/ionmich/latin-rectangles.git
|
|
44
|
+
cd latin-rectangles
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
### Command Line (CLI) Usage
|
|
50
|
+
|
|
51
|
+
Generate random derangement:
|
|
52
|
+
|
|
53
|
+
```console
|
|
54
|
+
> uv run python -m latin_rectangles --n 42
|
|
55
|
+
🎲 Generated Random Derangement for n=42
|
|
56
|
+
📊 Cycle structure: [2, 2, 4, 8, 26]
|
|
57
|
+
🔢 Number of extensions: 185,566,788,772,996,286,199,647,931,971,186,844,003,087,641,029,824
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Use specific cycle structure:
|
|
61
|
+
|
|
62
|
+
```console
|
|
63
|
+
> uv run python -m latin_rectangles --c "2,2,4"
|
|
64
|
+
⚙️ Specific Cycle Structure for n=8
|
|
65
|
+
📊 Cycle structure: [2, 2, 4]
|
|
66
|
+
🔢 Number of extensions: 4,744
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Enumerate all possible cycle structures:
|
|
70
|
+
|
|
71
|
+
```console
|
|
72
|
+
> uv run python -m latin_rectangles --n 8 --all
|
|
73
|
+
🔍 All Cycle Structures for n=8
|
|
74
|
+
📊 Found 7 possible structures with non-zero extensions:
|
|
75
|
+
|
|
76
|
+
1. [2, 2, 2, 2] → 4,752 extensions
|
|
77
|
+
2. [2, 2, 4] → 4,744 extensions
|
|
78
|
+
3. [2, 3, 3] → 4,740 extensions
|
|
79
|
+
4. [2, 6] → 4,740 extensions
|
|
80
|
+
5. [4, 4] → 4,740 extensions
|
|
81
|
+
6. [3, 5] → 4,738 extensions
|
|
82
|
+
7. [8] → 4,738 extensions
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Get help
|
|
86
|
+
|
|
87
|
+
```console
|
|
88
|
+
uv run latin-rectangles --help
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Python Library Usage
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from latin_rectangles import count_extensions, count_random_extensions
|
|
95
|
+
from latin_rectangles import generate_random_derangement
|
|
96
|
+
|
|
97
|
+
# Method 1: One-liner for random derangement
|
|
98
|
+
extensions = count_random_extensions(n=12)
|
|
99
|
+
print(f"Extensions: {extensions:,}")
|
|
100
|
+
|
|
101
|
+
# Method 2: Step-by-step with custom derangement
|
|
102
|
+
derangement = generate_random_derangement(n=10)
|
|
103
|
+
extensions = count_extensions(derangement)
|
|
104
|
+
print(f"Derangement {derangement[1:]} has {extensions:,} extensions")
|
|
105
|
+
|
|
106
|
+
# Method 3: With predefined derangement (1-indexed with dummy 0)
|
|
107
|
+
p = [0, 2, 3, 4, 5, 6, 7, 8, 1] # 8-cycle for n=8
|
|
108
|
+
extensions = count_extensions(p)
|
|
109
|
+
print(f"8-cycle has {extensions:,} extensions") # Output: 4,738
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Algorithm Details
|
|
113
|
+
|
|
114
|
+
### Mathematical Foundation
|
|
115
|
+
|
|
116
|
+
The algorithm leverages **rook polynomial theory** to solve the Latin rectangle extension problem:
|
|
117
|
+
|
|
118
|
+
1. **Input**: A derangement (permutation with no fixed points) representing the second row
|
|
119
|
+
2. **Cycle Decomposition**: Decompose the derangement into disjoint cycles
|
|
120
|
+
3. **Rook Polynomials**: Compute rook polynomial for each cycle structure
|
|
121
|
+
4. **Polynomial Multiplication**: Combine rook polynomials to get the final count
|
|
122
|
+
|
|
123
|
+
## API Reference
|
|
124
|
+
|
|
125
|
+
### Core Functions
|
|
126
|
+
|
|
127
|
+
#### `count_extensions(permutation: list[int]) -> int`
|
|
128
|
+
|
|
129
|
+
Counts the number of extensions for a given derangement.
|
|
130
|
+
|
|
131
|
+
**Parameters:**
|
|
132
|
+
|
|
133
|
+
- `permutation`: 1-indexed list representing a derangement (p[0] is dummy value)
|
|
134
|
+
|
|
135
|
+
**Returns:** Integer number of possible third rows
|
|
136
|
+
|
|
137
|
+
**Raises:** `ValueError` if input is not a derangement
|
|
138
|
+
|
|
139
|
+
#### `count_random_extensions(n: int) -> int`
|
|
140
|
+
|
|
141
|
+
Convenience function that generates a random derangement and counts its extensions.
|
|
142
|
+
|
|
143
|
+
**Parameters:**
|
|
144
|
+
|
|
145
|
+
- `n`: Size of the derangement (must be > 1)
|
|
146
|
+
|
|
147
|
+
**Returns:** Number of extensions for the randomly generated derangement
|
|
148
|
+
|
|
149
|
+
#### `generate_random_derangement(n: int) -> list[int]`
|
|
150
|
+
|
|
151
|
+
Generates a random derangement of size n.
|
|
152
|
+
|
|
153
|
+
**Parameters:**
|
|
154
|
+
|
|
155
|
+
- `n`: Size of the derangement
|
|
156
|
+
|
|
157
|
+
**Returns:** 1-indexed list representing the derangement
|
|
158
|
+
|
|
159
|
+
#### `find_cycle_decomposition(permutation: list[int]) -> list[list[int]]`
|
|
160
|
+
|
|
161
|
+
Finds the cycle decomposition of a permutation.
|
|
162
|
+
|
|
163
|
+
**Parameters:**
|
|
164
|
+
|
|
165
|
+
- `permutation`: 1-indexed permutation
|
|
166
|
+
|
|
167
|
+
**Returns:** List of cycles (each cycle is a list of indices)
|
|
168
|
+
|
|
169
|
+
## Examples
|
|
170
|
+
|
|
171
|
+
### Basic Usage Examples
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from latin_rectangles import count_extensions
|
|
175
|
+
|
|
176
|
+
# Example 1: Single 8-cycle
|
|
177
|
+
p_8_cycle = [0, 2, 3, 4, 5, 6, 7, 8, 1]
|
|
178
|
+
print(f"8-cycle: {count_extensions(p_8_cycle):,} extensions")
|
|
179
|
+
# Output: 8-cycle: 4,738 extensions
|
|
180
|
+
|
|
181
|
+
# Example 2: Two 4-cycles
|
|
182
|
+
p_4_4 = [0, 2, 3, 4, 1, 6, 7, 8, 5]
|
|
183
|
+
print(f"4,4-cycles: {count_extensions(p_4_4):,} extensions")
|
|
184
|
+
# Output: 4,4-cycles: 4,740 extensions
|
|
185
|
+
|
|
186
|
+
# Example 3: Four 2-cycles
|
|
187
|
+
p_2_2_2_2 = [0, 2, 1, 4, 3, 6, 5, 8, 7]
|
|
188
|
+
print(f"2,2,2,2-cycles: {count_extensions(p_2_2_2_2):,} extensions")
|
|
189
|
+
# Output: 2,2,2,2-cycles: 4,752 extensions
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Advanced Usage
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from latin_rectangles import generate_random_derangement, find_cycle_decomposition, count_extensions
|
|
196
|
+
|
|
197
|
+
# Generate and analyze a random derangement
|
|
198
|
+
n = 15
|
|
199
|
+
derangement = generate_random_derangement(n)
|
|
200
|
+
cycles = find_cycle_decomposition(derangement)
|
|
201
|
+
cycle_lengths = sorted([len(c) for c in cycles])
|
|
202
|
+
extensions = count_extensions(derangement)
|
|
203
|
+
|
|
204
|
+
print(f"n={n}")
|
|
205
|
+
print(f"Derangement: {derangement[1:]}")
|
|
206
|
+
print(f"Cycle structure: {cycle_lengths}")
|
|
207
|
+
print(f"Extensions: {extensions:,}")
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Batch Processing
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
from latin_rectangles import count_random_extensions
|
|
214
|
+
|
|
215
|
+
# Process multiple sizes
|
|
216
|
+
results = []
|
|
217
|
+
for n in range(5, 21):
|
|
218
|
+
extensions = count_random_extensions(n)
|
|
219
|
+
results.append((n, extensions))
|
|
220
|
+
print(f"n={n:2d}: {extensions:,} extensions")
|
|
221
|
+
|
|
222
|
+
# Find the size with the most extensions in this batch
|
|
223
|
+
max_n, max_extensions = max(results, key=lambda x: x[1])
|
|
224
|
+
print(f"Maximum: n={max_n} with {max_extensions:,} extensions")
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Development
|
|
228
|
+
|
|
229
|
+
### Running Tests
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
# Run the test suite
|
|
233
|
+
uv run pytest
|
|
234
|
+
|
|
235
|
+
# Run with coverage
|
|
236
|
+
uv run pytest --cov=latin_rectangles
|
|
237
|
+
|
|
238
|
+
# Run specific test
|
|
239
|
+
uv run pytest tests/test_main.py -v
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Code Quality
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
# Type checking
|
|
246
|
+
uv run mypy src/
|
|
247
|
+
|
|
248
|
+
# Linting
|
|
249
|
+
uv run ruff check src/
|
|
250
|
+
|
|
251
|
+
# Formatting
|
|
252
|
+
uv run ruff format src/
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Benchmarking
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
# Run performance benchmarks
|
|
259
|
+
uv run python benchmark.py
|
|
260
|
+
|
|
261
|
+
# Analyze complexity
|
|
262
|
+
uv run python complexity_analysis.py
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Contributing
|
|
266
|
+
|
|
267
|
+
Contributions are welcome! Please see [DEVELOPMENT.md](DEVELOPMENT.md) for development guidelines.
|
|
268
|
+
|
|
269
|
+
1. Fork the repository
|
|
270
|
+
2. Create a feature branch
|
|
271
|
+
3. Add tests for new functionality
|
|
272
|
+
4. Ensure all tests pass
|
|
273
|
+
5. Submit a pull request
|
|
274
|
+
|
|
275
|
+
## License
|
|
276
|
+
|
|
277
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
278
|
+
|
|
279
|
+
## Citation
|
|
280
|
+
|
|
281
|
+
If you use this library in your research, please cite:
|
|
282
|
+
|
|
283
|
+
```bibtex
|
|
284
|
+
@software{latin_rectangles,
|
|
285
|
+
title={Latin Rectangles Extension Counter},
|
|
286
|
+
author={Ioannis Michaloliakos},
|
|
287
|
+
year={2025},
|
|
288
|
+
url={https://github.com/ionmich/latin-rectangles}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
latin_rectangles/__init__.py,sha256=1JsPv1Ts57JLS9jBzlddwJlez5Lu_AMZXTs9hHcc_jU,1161
|
|
2
|
+
latin_rectangles/__main__.py,sha256=uudTFbM8QL7i8RELhIAdZSraEF6YrPLWXrG_tK358eQ,6554
|
|
3
|
+
latin_rectangles/derangements.py,sha256=psrC9BffezFnUrc4ls4JDiN8BgmiM_WGKGSfP0tUjA4,3045
|
|
4
|
+
latin_rectangles/extension_counting.py,sha256=aC8Wi3g6AH0w1Guzw3QD1ZV_ZzohxLbefqaOJ3LLDW4,1757
|
|
5
|
+
latin_rectangles/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
latin_rectangles/rook_polynomials.py,sha256=8cCzYevAAhypnAmYgsgDHZtypgYvcWa6f8lMe8hmcPg,2114
|
|
7
|
+
latin_rectangles-0.1.0.dist-info/METADATA,sha256=ur7BUyMCtOTK3fqKw29vdMt63hFrgo7ymP4NM_WU7xA,7798
|
|
8
|
+
latin_rectangles-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
9
|
+
latin_rectangles-0.1.0.dist-info/entry_points.txt,sha256=Y9TXJJPwmtEtDKSTeClqgM-6bYFsviRreYL-R30UUWE,68
|
|
10
|
+
latin_rectangles-0.1.0.dist-info/licenses/LICENSE,sha256=B7vY06ZLK_DfSNp5Pzqi4h5NAPAVzPI8T58yMNr9VDE,1078
|
|
11
|
+
latin_rectangles-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ioannis Michaloliakos
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|