| 1 | r""" |
| 2 | Counting, generating, and manipulating non-negative integer matrices |
| 3 | |
| 4 | Counting, generating, and manipulating non-negative integer matrices with |
| 5 | prescribed row sums and column sums. |
| 6 | |
| 7 | AUTHORS: |
| 8 | |
| 9 | - Franco Saliola |
| 10 | """ |
| 11 | #***************************************************************************** |
| 12 | # Copyright (C) 2012 Franco Saliola <saliola@gmail.com> |
| 13 | # |
| 14 | # Distributed under the terms of the GNU General Public License (GPL) |
| 15 | # http://www.gnu.org/licenses/ |
| 16 | #***************************************************************************** |
| 17 | |
| 18 | from sage.structure.unique_representation import UniqueRepresentation |
| 19 | from sage.structure.parent import Parent |
| 20 | from sage.categories.finite_enumerated_sets import FiniteEnumeratedSets |
| 21 | from sage.combinat.integer_list import IntegerListsLex |
| 22 | from sage.matrix.constructor import matrix |
| 23 | from sage.rings.integer_ring import ZZ |
| 24 | |
| 25 | class IntegerMatrices(UniqueRepresentation, Parent): |
| 26 | r""" |
| 27 | The class of non-negative integer matrices with |
| 28 | prescribed row sums and column sums. |
| 29 | |
| 30 | An *integer matrix* `m` with column sums `c := (c_1,...,c_k)` and row |
| 31 | sums `l := (l_1,...,l_n)` where `c_1+...+c_k` is equal to `l_1+...+l_n`, |
| 32 | is a `n \times k` matrix `m = (m_{i,j})` such that |
| 33 | `m_{1,j}+\dots+m_{n,j} = c_j`, for all `j` and |
| 34 | `m_{i,1}+\dots+m_{i,k} = l_i`, for all `i`. |
| 35 | |
| 36 | EXAMPLES: |
| 37 | |
| 38 | There are `6` integer matrices with row sums `[3,2,2]` and column sums |
| 39 | `[2,5]`:: |
| 40 | |
| 41 | sage: from sage.combinat.integer_matrices import IntegerMatrices |
| 42 | sage: IM = IntegerMatrices([3,2,2], [2,5]); IM |
| 43 | Non-negative integer matrices with row sums [3, 2, 2] and column sums [2, 5] |
| 44 | sage: IM.list() |
| 45 | [ |
| 46 | [2 1] [1 2] [1 2] [0 3] [0 3] [0 3] |
| 47 | [0 2] [1 1] [0 2] [2 0] [1 1] [0 2] |
| 48 | [0 2], [0 2], [1 1], [0 2], [1 1], [2 0] |
| 49 | ] |
| 50 | sage: IM.cardinality() |
| 51 | 6 |
| 52 | |
| 53 | """ |
| 54 | @staticmethod |
| 55 | def __classcall__(cls, row_sums, column_sums): |
| 56 | r""" |
| 57 | Normalize the inputs so that they are hashable. |
| 58 | |
| 59 | INPUT: |
| 60 | |
| 61 | - ``row_sums`` -- list, tuple, or anything defining a Composition |
| 62 | - ``column_sums`` -- list, tuple, or anything defining a Composition |
| 63 | |
| 64 | EXAMPLES:: |
| 65 | |
| 66 | sage: from sage.combinat.integer_matrices import IntegerMatrices |
| 67 | sage: IM = IntegerMatrices([4,4,5], [3,7,1,2]); IM |
| 68 | Non-negative integer matrices with row sums [4, 4, 5] and column sums [3, 7, 1, 2] |
| 69 | sage: IM = IntegerMatrices((4,4,5), (3,7,1,2)); IM |
| 70 | Non-negative integer matrices with row sums [4, 4, 5] and column sums [3, 7, 1, 2] |
| 71 | sage: IM = IntegerMatrices(Composition([4,4,5]), Composition([3,7,1,2])); IM |
| 72 | Non-negative integer matrices with row sums [4, 4, 5] and column sums [3, 7, 1, 2] |
| 73 | |
| 74 | """ |
| 75 | from sage.combinat.composition import Composition |
| 76 | row_sums = Composition(row_sums) |
| 77 | column_sums = Composition(column_sums) |
| 78 | return super(IntegerMatrices, cls).__classcall__(cls, row_sums, column_sums) |
| 79 | |
| 80 | def __init__(self, row_sums, column_sums): |
| 81 | r""" |
| 82 | Constructor of this class; for documentation, see |
| 83 | :class:`IntegerMatrices`. |
| 84 | |
| 85 | INPUT: |
| 86 | |
| 87 | - ``row_sums`` -- Composition |
| 88 | - ``column_sums`` -- Composition |
| 89 | |
| 90 | TESTS:: |
| 91 | |
| 92 | sage: from sage.combinat.integer_matrices import IntegerMatrices |
| 93 | sage: IM = IntegerMatrices([3,2,2], [2,5]); IM |
| 94 | Non-negative integer matrices with row sums [3, 2, 2] and column sums [2, 5] |
| 95 | sage: TestSuite(IM).run() |
| 96 | """ |
| 97 | self._row_sums = row_sums |
| 98 | self._col_sums = column_sums |
| 99 | Parent.__init__(self, category=FiniteEnumeratedSets()) |
| 100 | |
| 101 | def _repr_(self): |
| 102 | r""" |
| 103 | TESTS:: |
| 104 | |
| 105 | sage: from sage.combinat.integer_matrices import IntegerMatrices |
| 106 | sage: IntegerMatrices([3,2,2], [2,5])._repr_() |
| 107 | 'Non-negative integer matrices with row sums [3, 2, 2] and column sums [2, 5]' |
| 108 | """ |
| 109 | return "Non-negative integer matrices with row sums %s and column sums %s" % \ |
| 110 | (self._row_sums, self._col_sums) |
| 111 | |
| 112 | def __iter__(self): |
| 113 | r""" |
| 114 | An iterator for the integer matrices with the prescribed row sums and |
| 115 | columns sums. |
| 116 | |
| 117 | EXAMPLES:: |
| 118 | |
| 119 | sage: from sage.combinat.integer_matrices import IntegerMatrices |
| 120 | sage: IntegerMatrices([2,2], [1,2,1]).list() |
| 121 | [ |
| 122 | [1 1 0] [1 0 1] [0 2 0] [0 1 1] |
| 123 | [0 1 1], [0 2 0], [1 0 1], [1 1 0] |
| 124 | ] |
| 125 | sage: IntegerMatrices([0,0],[0,0,0]).list() |
| 126 | [ |
| 127 | [0 0 0] |
| 128 | [0 0 0] |
| 129 | ] |
| 130 | sage: IntegerMatrices([1,1],[1,1]).list() |
| 131 | [ |
| 132 | [1 0] [0 1] |
| 133 | [0 1], [1 0] |
| 134 | ] |
| 135 | |
| 136 | """ |
| 137 | for x in integer_matrices_generator(self._row_sums, self._col_sums): |
| 138 | yield matrix(ZZ, x) |
| 139 | |
| 140 | def __contains__(self, x): |
| 141 | r""" |
| 142 | Tests if ``x`` is an element of ``self``. |
| 143 | |
| 144 | INPUT: |
| 145 | |
| 146 | - ``x`` -- matrix |
| 147 | |
| 148 | EXAMPLES:: |
| 149 | |
| 150 | sage: from sage.combinat.integer_matrices import IntegerMatrices |
| 151 | sage: IM = IntegerMatrices([4], [1,2,1]) |
| 152 | sage: matrix([[1, 2, 1]]) in IM |
| 153 | True |
| 154 | sage: matrix(QQ, [[1, 2, 1]]) in IM |
| 155 | True |
| 156 | sage: matrix([[2, 1, 1]]) in IM |
| 157 | False |
| 158 | |
| 159 | TESTS:: |
| 160 | |
| 161 | sage: from sage.combinat.integer_matrices import IntegerMatrices |
| 162 | sage: IM = IntegerMatrices([4], [1,2,1]) |
| 163 | sage: [1, 2, 1] in IM |
| 164 | False |
| 165 | sage: matrix([[-1, 3, 1]]) in IM |
| 166 | False |
| 167 | |
| 168 | """ |
| 169 | from sage.matrix.matrix import Matrix |
| 170 | if not isinstance(x, Matrix): |
| 171 | return False |
| 172 | row_sums = [ZZ.zero()] * x.nrows() |
| 173 | col_sums = [ZZ.zero()] * x.ncols() |
| 174 | for i in range(x.nrows()): |
| 175 | for j in range(x.ncols()): |
| 176 | x_ij = x[i, j] |
| 177 | if x_ij not in ZZ or x_ij < 0: |
| 178 | return False |
| 179 | row_sums[i] += x_ij |
| 180 | col_sums[j] += x_ij |
| 181 | if row_sums[i] != self._row_sums[i]: |
| 182 | return False |
| 183 | if col_sums != self._col_sums: |
| 184 | return False |
| 185 | return True |
| 186 | |
| 187 | def cardinality(self): |
| 188 | r""" |
| 189 | The number of integer matrices with the prescribed row sums and columns |
| 190 | sums. |
| 191 | |
| 192 | EXAMPLES:: |
| 193 | |
| 194 | sage: from sage.combinat.integer_matrices import IntegerMatrices |
| 195 | sage: IntegerMatrices([2,5], [3,2,2]).cardinality() |
| 196 | 6 |
| 197 | sage: IntegerMatrices([1,1,1,1,1], [1,1,1,1,1]).cardinality() |
| 198 | 120 |
| 199 | sage: IntegerMatrices([2,2,2,2], [2,2,2,2]).cardinality() |
| 200 | 282 |
| 201 | sage: IntegerMatrices([4], [3]).cardinality() |
| 202 | 0 |
| 203 | sage: len(IntegerMatrices([0,0], [0]).list()) |
| 204 | 1 |
| 205 | |
| 206 | This method computes the cardinality using symmetric functions. Below |
| 207 | are the same examples, but computed by generating the actual matrices:: |
| 208 | |
| 209 | sage: from sage.combinat.integer_matrices import IntegerMatrices |
| 210 | sage: len(IntegerMatrices([2,5], [3,2,2]).list()) |
| 211 | 6 |
| 212 | sage: len(IntegerMatrices([1,1,1,1,1], [1,1,1,1,1]).list()) |
| 213 | 120 |
| 214 | sage: len(IntegerMatrices([2,2,2,2], [2,2,2,2]).list()) |
| 215 | 282 |
| 216 | sage: len(IntegerMatrices([4], [3]).list()) |
| 217 | 0 |
| 218 | sage: len(IntegerMatrices([0], [0]).list()) |
| 219 | 1 |
| 220 | |
| 221 | """ |
| 222 | from sage.combinat.sf.sf import SymmetricFunctions |
| 223 | from sage.combinat.partition import Partition |
| 224 | h = SymmetricFunctions(ZZ).homogeneous() |
| 225 | row_partition = Partition(sorted(self._row_sums, reverse=True)) |
| 226 | col_partition = Partition(sorted(self._col_sums, reverse=True)) |
| 227 | return h[row_partition].scalar(h[col_partition]) |
| 228 | |
| 229 | def row_sums(self): |
| 230 | r""" |
| 231 | The row sums of the integer matrices in ``self``. |
| 232 | |
| 233 | OUTPUT: |
| 234 | |
| 235 | - Composition |
| 236 | |
| 237 | EXAMPLES:: |
| 238 | |
| 239 | sage: from sage.combinat.integer_matrices import IntegerMatrices |
| 240 | sage: IM = IntegerMatrices([3,2,2], [2,5]) |
| 241 | sage: IM.row_sums() |
| 242 | [3, 2, 2] |
| 243 | """ |
| 244 | return self._row_sums |
| 245 | |
| 246 | def column_sums(self): |
| 247 | r""" |
| 248 | The column sums of the integer matrices in ``self``. |
| 249 | |
| 250 | OUTPUT: |
| 251 | |
| 252 | - Composition |
| 253 | |
| 254 | EXAMPLES:: |
| 255 | |
| 256 | sage: from sage.combinat.integer_matrices import IntegerMatrices |
| 257 | sage: IM = IntegerMatrices([3,2,2], [2,5]) |
| 258 | sage: IM.column_sums() |
| 259 | [2, 5] |
| 260 | """ |
| 261 | return self._col_sums |
| 262 | |
| 263 | def to_composition(self, x): |
| 264 | r""" |
| 265 | The composition corresponding to the integer matrix. |
| 266 | |
| 267 | This is the composition obtained by reading the entries of the matrix |
| 268 | from left to right along each row, and reading the rows from top to |
| 269 | bottom, ignore zeros. |
| 270 | |
| 271 | INPUT: |
| 272 | |
| 273 | - ``x`` -- matrix |
| 274 | |
| 275 | EXAMPLES:: |
| 276 | |
| 277 | sage: from sage.combinat.integer_matrices import IntegerMatrices |
| 278 | sage: IM = IntegerMatrices([3,2,2], [2,5]); IM |
| 279 | Non-negative integer matrices with row sums [3, 2, 2] and column sums [2, 5] |
| 280 | sage: IM.list() |
| 281 | [ |
| 282 | [2 1] [1 2] [1 2] [0 3] [0 3] [0 3] |
| 283 | [0 2] [1 1] [0 2] [2 0] [1 1] [0 2] |
| 284 | [0 2], [0 2], [1 1], [0 2], [1 1], [2 0] |
| 285 | ] |
| 286 | sage: for m in IM: print IM.to_composition(m) |
| 287 | [2, 1, 2, 2] |
| 288 | [1, 2, 1, 1, 2] |
| 289 | [1, 2, 2, 1, 1] |
| 290 | [3, 2, 2] |
| 291 | [3, 1, 1, 1, 1] |
| 292 | [3, 2, 2] |
| 293 | """ |
| 294 | from sage.combinat.composition import Composition |
| 295 | return Composition([entry for row in x for entry in row if entry != 0]) |
| 296 | |
| 297 | def integer_matrices_generator(row_sums, column_sums): |
| 298 | r""" |
| 299 | Recursively generate the integer matrices with the prescribed row sums and |
| 300 | column sums. |
| 301 | |
| 302 | INPUT: |
| 303 | |
| 304 | - ``row_sums`` -- list or tuple |
| 305 | - ``column_sums`` -- list or tuple |
| 306 | |
| 307 | OUTPUT: |
| 308 | |
| 309 | - an iterator producing a list of lists |
| 310 | |
| 311 | EXAMPLES:: |
| 312 | |
| 313 | sage: from sage.combinat.integer_matrices import integer_matrices_generator |
| 314 | sage: iter = integer_matrices_generator([3,2,2], [2,5]); iter |
| 315 | <generator object integer_matrices_generator at ...> |
| 316 | sage: for m in iter: print m |
| 317 | [[2, 1], [0, 2], [0, 2]] |
| 318 | [[1, 2], [1, 1], [0, 2]] |
| 319 | [[1, 2], [0, 2], [1, 1]] |
| 320 | [[0, 3], [2, 0], [0, 2]] |
| 321 | [[0, 3], [1, 1], [1, 1]] |
| 322 | [[0, 3], [0, 2], [2, 0]] |
| 323 | |
| 324 | """ |
| 325 | row_sums = list(row_sums) |
| 326 | column_sums = list(column_sums) |
| 327 | if sum(row_sums) != sum(column_sums): |
| 328 | raise StopIteration |
| 329 | if len(row_sums) == 0: |
| 330 | yield [] |
| 331 | elif len(row_sums) == 1: |
| 332 | yield [column_sums] |
| 333 | else: |
| 334 | for comp in IntegerListsLex(row_sums[0], len(column_sums), ceiling=column_sums): |
| 335 | t = [column_sums[i]-ci for (i, ci) in enumerate(comp)] |
| 336 | for mat in integer_matrices_generator(row_sums[1:], t): |
| 337 | yield [comp] + mat |