Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagelib
Path: blob/master/sage/combinat/dyck_word.py
4079 views
1
r"""
2
Dyck Words
3
4
AUTHORS:
5
6
- Mike Hansen
7
8
- Dan Drake (2008-05-30): DyckWordBacktracker support
9
10
- Florent Hivert (2009-02-01): Bijections with NonDecreasingParkingFunctions
11
"""
12
#*****************************************************************************
13
# Copyright (C) 2007 Mike Hansen <[email protected]>,
14
#
15
# Distributed under the terms of the GNU General Public License (GPL)
16
#
17
# This code is distributed in the hope that it will be useful,
18
# but WITHOUT ANY WARRANTY; without even the implied warranty of
19
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20
# General Public License for more details.
21
#
22
# The full text of the GPL is available at:
23
#
24
# http://www.gnu.org/licenses/
25
#*****************************************************************************
26
from combinat import CombinatorialClass, CombinatorialObject, catalan_number, InfiniteAbstractCombinatorialClass
27
from backtrack import GenericBacktracker
28
29
from sage.structure.parent import Parent
30
from sage.structure.unique_representation import UniqueRepresentation
31
from sage.categories.all import Posets
32
33
open_symbol = 1
34
close_symbol = 0
35
36
def replace_parens(x):
37
r"""
38
A map from ``'('`` to ``open_symbol`` and ``')'`` to ``close_symbol`` and
39
otherwise an error is raised. The values of the constants ``open_symbol``
40
and ``close_symbol`` are subject to change. This is the inverse map of
41
:func:`replace_symbols`.
42
43
INPUT:
44
45
- ``x`` -- either an opening or closing parenthesis.
46
47
OUTPUT:
48
49
- If ``x`` is an opening parenthesis, replace ``x`` with the constant
50
``open_symbol``.
51
52
- If ``x`` is a closing parenthesis, replace ``x`` with the constant
53
``close_symbol``.
54
55
- Raises a ``ValueError`` if ``x`` is neither an opening nor closing
56
parenthesis.
57
58
.. SEEALSO::
59
60
- :func:`replace_symbols`
61
62
EXAMPLES::
63
64
sage: from sage.combinat.dyck_word import replace_parens
65
sage: replace_parens('(')
66
1
67
sage: replace_parens(')')
68
0
69
sage: replace_parens(1)
70
Traceback (most recent call last):
71
...
72
ValueError
73
"""
74
if x == '(':
75
return open_symbol
76
elif x == ')':
77
return close_symbol
78
else:
79
raise ValueError
80
81
82
def replace_symbols(x):
83
r"""
84
A map from ``open_symbol`` to ``'('`` and ``close_symbol`` to ``')'`` and
85
otherwise an error is raised. The values of the constants ``open_symbol``
86
and ``close_symbol`` are subject to change. This is the inverse map of
87
:func:`replace_parens`.
88
89
INPUT:
90
91
- ``x`` -- either ``open_symbol`` or ``close_symbol``.
92
93
OUTPUT:
94
95
- If ``x`` is ``open_symbol``, replace ``x`` with ``'('``.
96
97
- If ``x`` is ``close_symbol``, replace ``x`` with ``')'``.
98
99
- If ``x`` is neither ``open_symbol`` nor ``close_symbol``, a
100
``ValueError`` is raised.
101
102
.. SEEALSO::
103
104
- :func:`replace_parens`
105
106
EXAMPLES::
107
108
sage: from sage.combinat.dyck_word import replace_symbols
109
sage: replace_symbols(1)
110
'('
111
sage: replace_symbols(0)
112
')'
113
sage: replace_symbols(3)
114
Traceback (most recent call last):
115
...
116
ValueError
117
"""
118
if x == open_symbol:
119
return '('
120
elif x == close_symbol:
121
return ')'
122
else:
123
raise ValueError
124
125
126
def DyckWord(dw=None, noncrossing_partition=None):
127
r"""
128
Returns a Dyck word object or a head of a Dyck word object if the Dyck
129
word is not complete.
130
131
EXAMPLES::
132
133
sage: dw = DyckWord([1, 0, 1, 0]); dw
134
[1, 0, 1, 0]
135
sage: print dw
136
()()
137
sage: print dw.height()
138
1
139
sage: dw.to_noncrossing_partition()
140
[[1], [2]]
141
142
::
143
144
sage: DyckWord('()()')
145
[1, 0, 1, 0]
146
sage: DyckWord('(())')
147
[1, 1, 0, 0]
148
sage: DyckWord('((')
149
[1, 1]
150
151
::
152
153
sage: DyckWord(noncrossing_partition=[[1],[2]])
154
[1, 0, 1, 0]
155
sage: DyckWord(noncrossing_partition=[[1,2]])
156
[1, 1, 0, 0]
157
158
TODO: In functions where a Dyck word is necessary, an error should be
159
raised (e.g. ``a_statistic``, ``b_statistic``)?
160
"""
161
if noncrossing_partition is not None:
162
return from_noncrossing_partition(noncrossing_partition)
163
164
elif isinstance(dw, str):
165
l = map(replace_parens, dw)
166
else:
167
l = dw
168
169
if isinstance(l, DyckWord_class):
170
return l
171
elif l in DyckWords() or is_a_prefix(l):
172
return DyckWord_class(l)
173
else:
174
raise ValueError, "invalid Dyck word"
175
176
class DyckWord_class(CombinatorialObject):
177
def __str__(self):
178
r"""
179
Returns a string consisting of matched parentheses corresponding to
180
the Dyck word.
181
182
EXAMPLES::
183
184
sage: print DyckWord([1, 0, 1, 0])
185
()()
186
sage: print DyckWord([1, 1, 0, 0])
187
(())
188
"""
189
return "".join(map(replace_symbols, [x for x in self]))
190
191
192
def size(self):
193
r"""
194
Returns the size which is the number of opening parentheses.
195
196
EXAMPLES::
197
198
sage: DyckWord([1, 0, 1, 0]).size()
199
2
200
sage: DyckWord([1, 0, 1, 1, 0]).size()
201
3
202
"""
203
return len(filter(lambda x: x == open_symbol, self))
204
205
def height(self):
206
r"""
207
Returns the height of the Dyck word.
208
209
We view the Dyck word as a Dyck path from `(0,0)` to
210
`(2n,0)` in the first quadrant by letting '1's represent
211
steps in the direction `(1,1)` and '0's represent steps in
212
the direction `(1,-1)`.
213
214
The height is the maximum `y`-coordinate reached.
215
216
EXAMPLES::
217
218
sage: DyckWord([]).height()
219
0
220
sage: DyckWord([1,0]).height()
221
1
222
sage: DyckWord([1, 1, 0, 0]).height()
223
2
224
sage: DyckWord([1, 1, 0, 1, 0]).height()
225
2
226
sage: DyckWord([1, 1, 0, 0, 1, 0]).height()
227
2
228
sage: DyckWord([1, 0, 1, 0]).height()
229
1
230
sage: DyckWord([1, 1, 0, 0, 1, 1, 1, 0, 0, 0]).height()
231
3
232
"""
233
# calling max(self.heights()) has a significant overhead (20%)
234
height = 0
235
height_max = 0
236
for letter in self:
237
if letter == open_symbol:
238
height += 1
239
height_max = max(height, height_max)
240
elif letter == close_symbol:
241
height -= 1
242
return height_max
243
244
def heights(self):
245
r"""
246
Returns the heights of the Dyck word.
247
248
We view the Dyck word as a Dyck path from `(0,0)` to
249
`(2n,0)` in the first quadrant by letting '1's represent
250
steps in the direction `(1,1)` and '0's represent steps in
251
the direction `(1,-1)`.
252
253
The heights is the sequence of `y`-coordinate reached.
254
255
EXAMPLES::
256
257
sage: DyckWord([]).heights()
258
(0,)
259
sage: DyckWord([1,0]).heights()
260
(0, 1, 0)
261
sage: DyckWord([1, 1, 0, 0]).heights()
262
(0, 1, 2, 1, 0)
263
sage: DyckWord([1, 1, 0, 1, 0]).heights()
264
(0, 1, 2, 1, 2, 1)
265
sage: DyckWord([1, 1, 0, 0, 1, 0]).heights()
266
(0, 1, 2, 1, 0, 1, 0)
267
sage: DyckWord([1, 0, 1, 0]).heights()
268
(0, 1, 0, 1, 0)
269
sage: DyckWord([1, 1, 0, 0, 1, 1, 1, 0, 0, 0]).heights()
270
(0, 1, 2, 1, 0, 1, 2, 3, 2, 1, 0)
271
272
.. seealso:: :meth:`from_heights` :meth:`min_from_heights`.
273
"""
274
height = 0
275
heights = [0]*(len(self)+1)
276
for i, letter in enumerate(self):
277
if letter == open_symbol:
278
height += 1
279
elif letter == close_symbol:
280
height -= 1
281
heights[i+1] = height
282
return tuple(heights)
283
284
@classmethod
285
def from_heights(cls, heights):
286
"""
287
Compute a dyck word knowing its heights.
288
289
We view the Dyck word as a Dyck path from `(0,0)` to
290
`(2n,0)` in the first quadrant by letting '1's represent
291
steps in the direction `(1,1)` and '0's represent steps in
292
the direction `(1,-1)`.
293
294
The :meth:`heights` is the sequence of `y`-coordinate reached.
295
296
EXAMPLES::
297
298
sage: from sage.combinat.dyck_word import DyckWord_class
299
sage: DyckWord_class.from_heights((0,))
300
[]
301
sage: DyckWord_class.from_heights((0, 1, 0))
302
[1, 0]
303
sage: DyckWord_class.from_heights((0, 1, 2, 1, 0))
304
[1, 1, 0, 0]
305
sage: DyckWord_class.from_heights((0, 1, 2, 1, 2, 1))
306
[1, 1, 0, 1, 0]
307
308
This also works for prefix of Dyck words::
309
310
sage: DyckWord_class.from_heights((0, 1, 2, 1))
311
[1, 1, 0]
312
313
.. seealso:: :meth:`heights` :meth:`min_from_heights`.
314
315
TESTS::
316
317
sage: all(dw == DyckWord_class.from_heights(dw.heights())
318
... for i in range(7) for dw in DyckWords(i))
319
True
320
321
sage: DyckWord_class.from_heights((1, 2, 1))
322
Traceback (most recent call last):
323
...
324
ValueError: heights must start with 0: (1, 2, 1)
325
sage: DyckWord_class.from_heights((0, 1, 4, 1))
326
Traceback (most recent call last):
327
...
328
ValueError: consecutive heights must only differ by 1: (0, 1, 4, 1)
329
"""
330
l1 = len(heights)-1
331
res = [0]*(l1)
332
if heights[0] != 0:
333
raise ValueError, "heights must start with 0: %s"%(heights,)
334
for i in range(l1):
335
if heights[i] == heights[i+1]-1:
336
res[i] = 1
337
elif heights[i] != heights[i+1]+1:
338
raise ValueError, (
339
"consecutive heights must only differ by 1: %s"%(heights,))
340
return cls(res)
341
342
@classmethod
343
def min_from_heights(cls, heights):
344
"""
345
Compute the smallest dyck word which lies some heights.
346
347
.. seealso:: :meth:`heights` :meth:`from_heights`.
348
349
EXAMPLES::
350
351
sage: from sage.combinat.dyck_word import DyckWord_class
352
sage: DyckWord_class.min_from_heights((0,))
353
[]
354
sage: DyckWord_class.min_from_heights((0, 1, 0))
355
[1, 0]
356
sage: DyckWord_class.min_from_heights((0, 0, 2, 0, 0))
357
[1, 1, 0, 0]
358
sage: DyckWord_class.min_from_heights((0, 0, 2, 0, 2, 0))
359
[1, 1, 0, 1, 0]
360
sage: DyckWord_class.min_from_heights((0, 0, 1, 0, 1, 0))
361
[1, 1, 0, 1, 0]
362
"""
363
# round heights to the smallest even-odd integer
364
heights = list(heights)
365
for i in range(0, len(heights), 2):
366
if heights[i] % 2 == 1:
367
heights[i]+=1
368
for i in range(1, len(heights), 2):
369
if heights[i] % 2 == 0:
370
heights[i]+=1
371
372
# smooth heights
373
for i in range(len(heights)-1):
374
if heights[i+1] < heights[i]:
375
heights[i+1] = heights[i]-1
376
for i in range(len(heights)-1, 0, -1):
377
if heights[i] > heights[i-1]:
378
heights[i-1] = heights[i]-1
379
return cls.from_heights(heights)
380
381
382
def associated_parenthesis(self, pos):
383
r"""
384
Report the position for the parenthesis that matches the one at
385
position ``pos``.
386
387
INPUT:
388
389
- ``pos`` - the index of the parenthesis in the list.
390
391
OUTPUT:
392
393
Integer representing the index of the matching parenthesis. If no
394
parenthesis matches, return ``None``.
395
396
EXAMPLES::
397
398
sage: DyckWord([1, 0]).associated_parenthesis(0)
399
1
400
sage: DyckWord([1, 0, 1, 0]).associated_parenthesis(0)
401
1
402
sage: DyckWord([1, 0, 1, 0]).associated_parenthesis(1)
403
0
404
sage: DyckWord([1, 0, 1, 0]).associated_parenthesis(2)
405
3
406
sage: DyckWord([1, 0, 1, 0]).associated_parenthesis(3)
407
2
408
sage: DyckWord([1, 1, 0, 0]).associated_parenthesis(0)
409
3
410
sage: DyckWord([1, 1, 0, 0]).associated_parenthesis(2)
411
1
412
sage: DyckWord([1, 1, 0]).associated_parenthesis(1)
413
2
414
sage: DyckWord([1, 1]).associated_parenthesis(0)
415
"""
416
d = 0
417
height = 0
418
if pos >= len(self):
419
raise ValueError, "invalid index"
420
421
if self[pos] == open_symbol:
422
d += 1
423
height += 1
424
elif self[pos] == close_symbol:
425
d -= 1
426
height -= 1
427
else:
428
raise ValueError, "unknown symbol %s"%self[pos-1]
429
430
while height != 0:
431
pos += d
432
if pos < 0 or pos >= len(self):
433
return None
434
if self[pos] == open_symbol:
435
height += 1
436
elif self[pos] == close_symbol:
437
height -= 1
438
439
return pos
440
441
def to_noncrossing_partition(self):
442
r"""
443
Bijection of Biane from Dyck words to non-crossing partitions.
444
Thanks to Mathieu Dutour for describing the bijection.
445
446
EXAMPLES::
447
448
sage: DyckWord([]).to_noncrossing_partition()
449
[]
450
sage: DyckWord([1, 0]).to_noncrossing_partition()
451
[[1]]
452
sage: DyckWord([1, 1, 0, 0]).to_noncrossing_partition()
453
[[1, 2]]
454
sage: DyckWord([1, 1, 1, 0, 0, 0]).to_noncrossing_partition()
455
[[1, 2, 3]]
456
sage: DyckWord([1, 0, 1, 0, 1, 0]).to_noncrossing_partition()
457
[[1], [2], [3]]
458
sage: DyckWord([1, 1, 0, 1, 0, 0]).to_noncrossing_partition()
459
[[2], [1, 3]]
460
"""
461
partition = []
462
stack = []
463
i = 0
464
p = 1
465
466
#Invariants:
467
# - self[i] = 0
468
# - p is the number of opening parens at position i
469
470
while i < len(self):
471
stack.append(p)
472
j = i + 1
473
while j < len(self) and self[j] == close_symbol:
474
j += 1
475
476
#Now j points to the next 1 or past the end of self
477
nz = j - (i+1) # the number of )'s between i and j
478
if nz > 0:
479
# Remove the nz last elements of stack and
480
# make a new part in partition
481
if nz > len(stack):
482
raise ValueError, "incorrect dyck word"
483
484
partition.append( stack[-nz:] )
485
486
stack = stack[: -nz]
487
i = j
488
p += 1
489
490
if len(stack) > 0:
491
raise ValueError, "incorrect dyck word"
492
493
return partition
494
495
def to_ordered_tree(self):
496
"""
497
TESTS::
498
499
sage: DyckWord([1, 1, 0, 0]).to_ordered_tree()
500
Traceback (most recent call last):
501
...
502
NotImplementedError: TODO
503
"""
504
raise NotImplementedError, "TODO"
505
506
def to_triangulation(self):
507
"""
508
TESTS::
509
510
sage: DyckWord([1, 1, 0, 0]).to_triangulation()
511
Traceback (most recent call last):
512
...
513
NotImplementedError: TODO
514
"""
515
raise NotImplementedError, "TODO"
516
517
def to_non_decreasing_parking_function(self):
518
"""
519
Bijection to :class:`non-decreasing parking
520
functions<sage.combinat.non_decreasing_parking_function.NonDecreasingParkingFunctions>`. See
521
there the method
522
:meth:`~sage.combinat.non_decreasing_parking_function.NonDecreasingParkingFunction.to_dyck_word`
523
for more information.
524
525
EXAMPLES::
526
527
sage: DyckWord([]).to_non_decreasing_parking_function()
528
[]
529
sage: DyckWord([1,0]).to_non_decreasing_parking_function()
530
[1]
531
sage: DyckWord([1,1,0,0]).to_non_decreasing_parking_function()
532
[1, 1]
533
sage: DyckWord([1,0,1,0]).to_non_decreasing_parking_function()
534
[1, 2]
535
sage: DyckWord([1,0,1,1,0,1,0,0,1,0]).to_non_decreasing_parking_function()
536
[1, 2, 2, 3, 5]
537
538
TESTS::
539
540
sage: ld=DyckWords(5);
541
sage: list(ld) == [dw.to_non_decreasing_parking_function().to_dyck_word() for dw in ld]
542
True
543
"""
544
from sage.combinat.non_decreasing_parking_function import NonDecreasingParkingFunction
545
return NonDecreasingParkingFunction.from_dyck_word(self)
546
547
@classmethod
548
def from_non_decreasing_parking_function(cls, pf):
549
r"""
550
Bijection from :class:`non-decreasing parking
551
functions<sage.combinat.non_decreasing_parking_function.NonDecreasingParkingFunctions>`. See
552
there the method
553
:meth:`~sage.combinat.non_decreasing_parking_function.NonDecreasingParkingFunction.to_dyck_word`
554
for more information.
555
556
EXAMPLES::
557
558
sage: from sage.combinat.dyck_word import DyckWord_class
559
sage: DyckWord_class.from_non_decreasing_parking_function([])
560
[]
561
sage: DyckWord_class.from_non_decreasing_parking_function([1])
562
[1, 0]
563
sage: DyckWord_class.from_non_decreasing_parking_function([1,1])
564
[1, 1, 0, 0]
565
sage: DyckWord_class.from_non_decreasing_parking_function([1,2])
566
[1, 0, 1, 0]
567
sage: DyckWord_class.from_non_decreasing_parking_function([1,1,1])
568
[1, 1, 1, 0, 0, 0]
569
sage: DyckWord_class.from_non_decreasing_parking_function([1,2,3])
570
[1, 0, 1, 0, 1, 0]
571
sage: DyckWord_class.from_non_decreasing_parking_function([1,1,3,3,4,6,6])
572
[1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0]
573
574
TESTS::
575
576
sage: DyckWord_class.from_non_decreasing_parking_function(NonDecreasingParkingFunction([]))
577
[]
578
sage: DyckWord_class.from_non_decreasing_parking_function(NonDecreasingParkingFunction([1,1,3,3,4,6,6]))
579
[1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0]
580
"""
581
from sage.combinat.non_decreasing_parking_function import NonDecreasingParkingFunction
582
if isinstance(pf, NonDecreasingParkingFunction):
583
pf = pf._list[:]
584
else:
585
pf = pf[:]
586
pf.append(len(pf)+1)
587
res = []
588
for i in range(len(pf)-1):
589
res.extend([1]+([0]*(pf[i+1]-pf[i])))
590
return cls(res)
591
592
593
594
def peaks(self):
595
r"""
596
Returns a list of the positions of the peaks of a Dyck word. A peak
597
is 1 followed by a 0. Note that this does not agree with the
598
definition given by Haglund in: The `q,t`-Catalan Numbers
599
and the Space of Diagonal Harmonics: With an Appendix on the
600
Combinatorics of Macdonald Polynomials - James Haglund, University
601
of Pennsylvania, Philadelphia - AMS, 2008, 167 pp.
602
603
EXAMPLES::
604
605
sage: DyckWord([1, 0, 1, 0]).peaks()
606
[0, 2]
607
sage: DyckWord([1, 1, 0, 0]).peaks()
608
[1]
609
sage: DyckWord([1,1,0,1,0,1,0,0]).peaks() # Haglund's def gives 2
610
[1, 3, 5]
611
"""
612
return [i for i in range(len(self)-1) if self[i] == open_symbol and self[i+1] == close_symbol]
613
614
def valleys(self):
615
r"""
616
Returns a list of the positions of the valleys of a Dyck
617
word. A valley is 0 followed by a 1.
618
619
EXAMPLES::
620
621
sage: DyckWord([1, 0, 1, 0]).valleys()
622
[1]
623
sage: DyckWord([1, 1, 0, 0]).valleys()
624
[]
625
sage: DyckWord([1,1,0,1,0,1,0,0]).valleys()
626
[2, 4]
627
"""
628
return [i for i in xrange(len(self)-1) if self[i] == close_symbol and self[i+1] == open_symbol]
629
630
def to_tableau(self):
631
r"""
632
Returns a standard tableau of length less than or equal to 2 with the
633
size the same as the length of the list. The standard tableau will be
634
rectangular iff ``self`` is a complete Dyck word.
635
636
EXAMPLES::
637
638
sage: DyckWord([]).to_tableau()
639
[]
640
sage: DyckWord([1, 0]).to_tableau()
641
[[2], [1]]
642
sage: DyckWord([1, 1, 0, 0]).to_tableau()
643
[[3, 4], [1, 2]]
644
sage: DyckWord([1, 0, 1, 0]).to_tableau()
645
[[2, 4], [1, 3]]
646
sage: DyckWord([1]).to_tableau()
647
[[1]]
648
sage: DyckWord([1, 0, 1]).to_tableau()
649
[[2], [1, 3]]
650
651
TODO: better name? ``to_standard_tableau``? and should *actually*
652
return a Tableau object?
653
"""
654
open_positions = []
655
close_positions = []
656
657
for i in range(len(self)):
658
if self[i] == open_symbol:
659
open_positions.append(i+1)
660
else:
661
close_positions.append(i+1)
662
return filter(lambda x: x != [], [ close_positions, open_positions ])
663
664
def a_statistic(self):
665
"""
666
Returns the a-statistic for the Dyck word corresponding to the area
667
of the Dyck path.
668
669
One can view a balanced Dyck word as a lattice path from
670
`(0,0)` to `(n,n)` in the first quadrant by letting
671
'1's represent steps in the direction `(1,0)` and '0's
672
represent steps in the direction `(0,1)`. The resulting
673
path will remain weakly above the diagonal `y = x`.
674
675
The a-statistic, or area statistic, is the number of complete
676
squares in the integer lattice which are below the path and above
677
the line `y = x`. The 'half-squares' directly above the
678
line `y=x` do not contribute to this statistic.
679
680
EXAMPLES::
681
682
sage: dw = DyckWord([1,0,1,0])
683
sage: dw.a_statistic() # 2 half-squares, 0 complete squares
684
0
685
686
::
687
688
sage: dw = DyckWord([1,1,1,0,1,1,1,0,0,0,1,1,0,0,1,0,0,0])
689
sage: dw.a_statistic()
690
19
691
692
::
693
694
sage: DyckWord([1,1,1,1,0,0,0,0]).a_statistic()
695
6
696
sage: DyckWord([1,1,1,0,1,0,0,0]).a_statistic()
697
5
698
sage: DyckWord([1,1,1,0,0,1,0,0]).a_statistic()
699
4
700
sage: DyckWord([1,1,1,0,0,0,1,0]).a_statistic()
701
3
702
sage: DyckWord([1,0,1,1,0,1,0,0]).a_statistic()
703
2
704
sage: DyckWord([1,1,0,1,1,0,0,0]).a_statistic()
705
4
706
sage: DyckWord([1,1,0,0,1,1,0,0]).a_statistic()
707
2
708
sage: DyckWord([1,0,1,1,1,0,0,0]).a_statistic()
709
3
710
sage: DyckWord([1,0,1,1,0,0,1,0]).a_statistic()
711
1
712
sage: DyckWord([1,0,1,0,1,1,0,0]).a_statistic()
713
1
714
sage: DyckWord([1,1,0,0,1,0,1,0]).a_statistic()
715
1
716
sage: DyckWord([1,1,0,1,0,0,1,0]).a_statistic()
717
2
718
sage: DyckWord([1,1,0,1,0,1,0,0]).a_statistic()
719
3
720
sage: DyckWord([1,0,1,0,1,0,1,0]).a_statistic()
721
0
722
"""
723
above = 0
724
diagonal = 0
725
a = 0
726
for move in self:
727
if move == 1:
728
above += 1
729
elif move == 0:
730
diagonal += 1
731
a += above - diagonal
732
return a
733
734
def b_statistic(self):
735
r"""
736
Returns the b-statistic for the Dyck word corresponding to the bounce
737
statistic of the Dyck word.
738
739
One can view a balanced Dyck word as a lattice path from `(0,0)` to
740
`(n,n)` in the first quadrant by letting '1's represent steps in
741
the direction `(0,1)` and '0's represent steps in the direction
742
`(1,0)`. The resulting path will remain weakly above the diagonal
743
`y = x`.
744
745
We describe the b-statistic of such a path in terms of what is
746
known as the "bounce path".
747
748
We can think of our bounce path as describing the trail of a billiard
749
ball shot West from (n, n), which "bounces" down whenever it
750
encounters a vertical step and "bounces" left when it encounters the
751
line y = x.
752
753
The bouncing ball will strike the diagonal at places
754
755
.. MATH::
756
757
(0, 0), (j_1, j_1), (j_2, j_2), \dots , (j_r-1, j_r-1), (j_r, j_r)
758
=
759
(n, n).
760
761
We define the b-statistic to be the sum `\sum_{i=1}^{r-1} j_i`.
762
763
EXAMPLES::
764
765
sage: dw = DyckWord([1,1,1,0,1,1,1,0,0,0,1,1,0,0,1,0,0,0])
766
sage: dw.b_statistic()
767
7
768
769
::
770
771
sage: DyckWord([1,1,1,1,0,0,0,0]).b_statistic()
772
0
773
sage: DyckWord([1,1,1,0,1,0,0,0]).b_statistic()
774
1
775
sage: DyckWord([1,1,1,0,0,1,0,0]).b_statistic()
776
2
777
sage: DyckWord([1,1,1,0,0,0,1,0]).b_statistic()
778
3
779
sage: DyckWord([1,0,1,1,0,1,0,0]).b_statistic()
780
3
781
sage: DyckWord([1,1,0,1,1,0,0,0]).b_statistic()
782
1
783
sage: DyckWord([1,1,0,0,1,1,0,0]).b_statistic()
784
2
785
sage: DyckWord([1,0,1,1,1,0,0,0]).b_statistic()
786
1
787
sage: DyckWord([1,0,1,1,0,0,1,0]).b_statistic()
788
4
789
sage: DyckWord([1,0,1,0,1,1,0,0]).b_statistic()
790
3
791
sage: DyckWord([1,1,0,0,1,0,1,0]).b_statistic()
792
5
793
sage: DyckWord([1,1,0,1,0,0,1,0]).b_statistic()
794
4
795
sage: DyckWord([1,1,0,1,0,1,0,0]).b_statistic()
796
2
797
sage: DyckWord([1,0,1,0,1,0,1,0]).b_statistic()
798
6
799
800
REFERENCES:
801
802
- [1] The `q,t`-Catalan Numbers and the Space of
803
Diagonal Harmonics: With an Appendix on the Combinatorics of
804
Macdonald Polynomials - James Haglund, University of
805
Pennsylvania, Philadelphia - AMS, 2008, 167 pp.
806
"""
807
x_pos = len(self)/2
808
y_pos = len(self)/2
809
810
b = 0
811
812
mode = "left"
813
makeup_steps = 0
814
l = self._list[:]
815
l.reverse()
816
817
for move in l:
818
#print x_pos, y_pos, mode, move
819
if mode == "left":
820
if move == 0:
821
x_pos -= 1
822
elif move == 1:
823
y_pos -= 1
824
if x_pos == y_pos:
825
b += x_pos
826
else:
827
mode = "drop"
828
elif mode == "drop":
829
if move == 0:
830
makeup_steps += 1
831
elif move == 1:
832
y_pos -= 1
833
if x_pos == y_pos:
834
b += x_pos
835
mode = "left"
836
x_pos -= makeup_steps
837
makeup_steps = 0
838
839
return b
840
841
842
def DyckWords(k1=None, k2=None):
843
r"""
844
Returns the combinatorial class of Dyck words. A Dyck word is a
845
sequence `(w_1, ..., w_n)` consisting of '1's and '0's,
846
with the property that for any `i` with
847
`1 \le i \le n`, the sequence `(w_1,...,w_i)`
848
contains at least as many `1` s as `0` .
849
850
A Dyck word is balanced if the total number of '1's is equal to the
851
total number of '0's. The number of balanced Dyck words of length
852
`2k` is given by the Catalan number `C_k`.
853
854
EXAMPLES: If neither k1 nor k2 are specified, then DyckWords
855
returns the combinatorial class of all balanced Dyck words.
856
857
::
858
859
sage: DW = DyckWords(); DW
860
Dyck words
861
sage: [] in DW
862
True
863
sage: [1, 0, 1, 0] in DW
864
True
865
sage: [1, 1, 0] in DW
866
False
867
868
If just k1 is specified, then it returns the combinatorial class of
869
balanced Dyck words with k1 opening parentheses and k1 closing
870
parentheses.
871
872
::
873
874
sage: DW2 = DyckWords(2); DW2
875
Dyck words with 2 opening parentheses and 2 closing parentheses
876
sage: DW2.first()
877
[1, 0, 1, 0]
878
sage: DW2.last()
879
[1, 1, 0, 0]
880
sage: DW2.cardinality()
881
2
882
sage: DyckWords(100).cardinality() == catalan_number(100)
883
True
884
885
If k2 is specified in addition to k1, then it returns the
886
combinatorial class of Dyck words with k1 opening parentheses and
887
k2 closing parentheses.
888
889
::
890
891
sage: DW32 = DyckWords(3,2); DW32
892
Dyck words with 3 opening parentheses and 2 closing parentheses
893
sage: DW32.list()
894
[[1, 0, 1, 0, 1],
895
[1, 0, 1, 1, 0],
896
[1, 1, 0, 0, 1],
897
[1, 1, 0, 1, 0],
898
[1, 1, 1, 0, 0]]
899
"""
900
if k1 is None and k2 is None:
901
return DyckWords_all()
902
else:
903
if k1 < 0 or (k2 is not None and k2 < 0):
904
raise ValueError, "k1 (= %s) and k2 (= %s) must be nonnegative, with k1 >= k2."%(k1, k2)
905
if k2 is not None and k1 < k2:
906
raise ValueError, "k1 (= %s) must be >= k2 (= %s)"%(k1, k2)
907
if k2 is None:
908
return DyckWords_size(k1, k1)
909
else:
910
return DyckWords_size(k1, k2)
911
912
class DyckWords_all(InfiniteAbstractCombinatorialClass):
913
def __init__(self):
914
r"""
915
TESTS::
916
917
sage: DW = DyckWords()
918
sage: DW == loads(dumps(DW))
919
True
920
"""
921
pass
922
923
def __repr__(self):
924
r"""
925
TESTS::
926
927
sage: repr(DyckWords())
928
'Dyck words'
929
"""
930
return "Dyck words"
931
932
def __contains__(self, x):
933
r"""
934
TESTS::
935
936
sage: [] in DyckWords()
937
True
938
sage: [1] in DyckWords()
939
False
940
sage: [0] in DyckWords()
941
False
942
sage: [1, 0] in DyckWords()
943
True
944
"""
945
if isinstance(x, DyckWord_class):
946
return True
947
948
if not isinstance(x, list):
949
return False
950
951
if len(x) % 2 != 0:
952
return False
953
954
return is_a(x)
955
956
def _infinite_cclass_slice(self, n):
957
r"""
958
Needed by InfiniteAbstractCombinatorialClass to build __iter__.
959
960
TESTS::
961
962
sage: DyckWords()._infinite_cclass_slice(4) == DyckWords(4)
963
True
964
sage: it = iter(DyckWords()) # indirect doctest
965
sage: [it.next() for i in range(10)]
966
[[], [1, 0], [1, 0, 1, 0], [1, 1, 0, 0], [1, 0, 1, 0, 1, 0], [1, 0, 1, 1, 0, 0], [1, 1, 0, 0, 1, 0], [1, 1, 0, 1, 0, 0], [1, 1, 1, 0, 0, 0], [1, 0, 1, 0, 1, 0, 1, 0]]
967
"""
968
return DyckWords_size(n, n)
969
970
def _an_element_(self):
971
"""
972
TESTS::
973
974
sage: DyckWords().an_element()
975
[1, 0, 1, 0, 1, 0, 1, 0, 1, 0]
976
"""
977
return self._infinite_cclass_slice(5).an_element()
978
979
class height_poset(UniqueRepresentation, Parent):
980
r"""
981
The poset of dyck word compared by heights
982
"""
983
def __init__(self):
984
"""
985
TESTS::
986
987
sage: poset = DyckWords().height_poset()
988
sage: TestSuite(poset).run()
989
"""
990
Parent.__init__(self,
991
facade = DyckWords_all(),
992
category = Posets())
993
def _an_element_(self):
994
"""
995
TESTS::
996
997
sage: DyckWords().height_poset().an_element() # indirect doctest
998
[1, 0, 1, 0, 1, 0, 1, 0, 1, 0]
999
1000
"""
1001
return DyckWords_all().an_element()
1002
1003
def __call__(self, obj):
1004
"""
1005
TESTS::
1006
1007
sage: poset = DyckWords().height_poset()
1008
sage: poset([1,0,1,0])
1009
[1, 0, 1, 0]
1010
"""
1011
return DyckWords_all()(obj)
1012
1013
def le(self, dw1, dw2):
1014
r"""
1015
Compare two dyck words.
1016
1017
EXAMPLES::
1018
1019
sage: poset = DyckWords().height_poset()
1020
sage: poset.le(DyckWord([]), DyckWord([]))
1021
True
1022
sage: poset.le(DyckWord([1,0]), DyckWord([1,0]))
1023
True
1024
sage: poset.le(DyckWord([1,0,1,0]), DyckWord([1,1,0,0]))
1025
True
1026
sage: poset.le(DyckWord([1,1,0,0]), DyckWord([1,0,1,0]))
1027
False
1028
sage: [poset.le(dw1, dw2)
1029
... for dw1 in DyckWords(3) for dw2 in DyckWords(3)]
1030
[True, True, True, True, True, False, True, False, True, True, False, False, True, True, True, False, False, False, True, True, False, False, False, False, True]
1031
"""
1032
assert len(dw1)==len(dw2), "Length mismatch: %s and %s"%(dw1, dw2)
1033
sh = dw1.heights()
1034
oh = dw2.heights()
1035
return all(sh[i] <= oh[i] for i in range(len(dw1)))
1036
1037
1038
class DyckWordBacktracker(GenericBacktracker):
1039
r"""
1040
DyckWordBacktracker: this class is an iterator for all Dyck words
1041
with n opening parentheses and n - endht closing parentheses using
1042
the backtracker class. It is used by the DyckWords_size class.
1043
1044
This is not really meant to be called directly, partially because
1045
it fails in a couple corner cases: DWB(0) yields [0], not the
1046
empty word, and DWB(k, k+1) yields something (it shouldn't yield
1047
anything). This could be fixed with a sanity check in _rec(), but
1048
then we'd be doing the sanity check *every time* we generate new
1049
objects; instead, we do *one* sanity check in DyckWords and assume
1050
here that the sanity check has already been made.
1051
1052
AUTHOR:
1053
1054
- Dan Drake (2008-05-30)
1055
"""
1056
def __init__(self, k1, k2):
1057
r"""
1058
TESTS::
1059
1060
sage: from sage.combinat.dyck_word import DyckWordBacktracker
1061
sage: len(list(DyckWordBacktracker(5, 5)))
1062
42
1063
sage: len(list(DyckWordBacktracker(6,4)))
1064
90
1065
sage: len(list(DyckWordBacktracker(7,0)))
1066
1
1067
"""
1068
GenericBacktracker.__init__(self, [], (0, 0))
1069
# note that the comments in this class think of our objects as
1070
# Dyck paths, not words; having k1 opening parens and k2 closing
1071
# parens corresponds to paths of length k1 + k2 ending at height
1072
# k1 - k2.
1073
self.n = k1 + k2
1074
self.endht = k1 - k2
1075
1076
def _rec(self, path, state):
1077
r"""
1078
TESTS::
1079
1080
sage: from sage.combinat.dyck_word import DyckWordBacktracker
1081
sage: dwb = DyckWordBacktracker(3, 3)
1082
sage: list(dwb._rec([1,1,0],(3, 2)))
1083
[([1, 1, 0, 0], (4, 1), False), ([1, 1, 0, 1], (4, 3), False)]
1084
sage: list(dwb._rec([1,1,0,0],(4, 0)))
1085
[([1, 1, 0, 0, 1], (5, 1), False)]
1086
sage: list(DyckWordBacktracker(4, 4)._rec([1,1,1,1],(4, 4)))
1087
[([1, 1, 1, 1, 0], (5, 3), False)]
1088
"""
1089
len, ht = state
1090
1091
if len < self.n - 1:
1092
# if length is less than n-1, new path won't have length n, so
1093
# don't yield it, and keep building paths
1094
1095
# if the path isn't too low and is not touching the x-axis, we can
1096
# yield a path with a downstep at the end
1097
if ht > (self.endht - (self.n - len)) and ht > 0:
1098
yield path + [0], (len + 1, ht - 1), False
1099
1100
# if the path isn't too high, it can also take an upstep
1101
if ht < (self.endht + (self.n - len)):
1102
yield path + [1], (len + 1, ht + 1), False
1103
else:
1104
# length is n - 1, so add a single step (up or down,
1105
# according to current height and endht), don't try to
1106
# construct more paths, and yield the path
1107
if ht < self.endht:
1108
yield path + [1], None, True
1109
else:
1110
yield path + [0], None, True
1111
1112
1113
class DyckWords_size(CombinatorialClass):
1114
def __init__(self, k1, k2=None):
1115
r"""
1116
TESTS::
1117
1118
sage: DW4 = DyckWords(4)
1119
sage: DW4 == loads(dumps(DW4))
1120
True
1121
sage: DW42 = DyckWords(4,2)
1122
sage: DW42 == loads(dumps(DW42))
1123
True
1124
"""
1125
self.k1 = k1
1126
self.k2 = k2
1127
1128
1129
def __repr__(self):
1130
r"""
1131
TESTS::
1132
1133
sage: repr(DyckWords(4))
1134
'Dyck words with 4 opening parentheses and 4 closing parentheses'
1135
"""
1136
return "Dyck words with %s opening parentheses and %s closing parentheses"%(self.k1, self.k2)
1137
1138
def cardinality(self):
1139
r"""
1140
Returns the number of Dyck words of size n, i.e. the n-th Catalan
1141
number.
1142
1143
EXAMPLES::
1144
1145
sage: DyckWords(4).cardinality()
1146
14
1147
sage: ns = range(9)
1148
sage: dws = [DyckWords(n) for n in ns]
1149
sage: all([ dw.cardinality() == len(dw.list()) for dw in dws])
1150
True
1151
"""
1152
if self.k2 == self.k1:
1153
return catalan_number(self.k1)
1154
else:
1155
return len(self.list())
1156
1157
def __contains__(self, x):
1158
r"""
1159
EXAMPLES::
1160
1161
sage: [1, 0] in DyckWords(1)
1162
True
1163
sage: [1, 0] in DyckWords(2)
1164
False
1165
sage: [1, 1, 0, 0] in DyckWords(2)
1166
True
1167
sage: [1, 0, 0, 1] in DyckWords(2)
1168
False
1169
sage: [1, 0, 0, 1] in DyckWords(2,2)
1170
False
1171
sage: [1, 0, 1, 0] in DyckWords(2,2)
1172
True
1173
sage: [1, 0, 1, 0, 1] in DyckWords(3,2)
1174
True
1175
sage: [1, 0, 1, 1, 0] in DyckWords(3,2)
1176
True
1177
sage: [1, 0, 1, 1] in DyckWords(3,1)
1178
True
1179
"""
1180
return is_a(x, self.k1, self.k2)
1181
1182
def list(self):
1183
"""
1184
Returns a list of all the Dyck words with ``k1`` opening and ``k2``
1185
closing parentheses.
1186
1187
EXAMPLES::
1188
1189
sage: DyckWords(0).list()
1190
[[]]
1191
sage: DyckWords(1).list()
1192
[[1, 0]]
1193
sage: DyckWords(2).list()
1194
[[1, 0, 1, 0], [1, 1, 0, 0]]
1195
"""
1196
return list(self)
1197
1198
def __iter__(self):
1199
r"""
1200
Returns an iterator for Dyck words with ``k1`` opening and ``k2``
1201
closing parentheses.
1202
1203
EXAMPLES::
1204
1205
sage: [ w for w in DyckWords(0) ]
1206
[[]]
1207
sage: [ w for w in DyckWords(1) ]
1208
[[1, 0]]
1209
sage: [ w for w in DyckWords(2) ]
1210
[[1, 0, 1, 0], [1, 1, 0, 0]]
1211
sage: len([ 'x' for _ in DyckWords(5) ])
1212
42
1213
"""
1214
if self.k1 == 0:
1215
yield DyckWord_class([])
1216
elif self.k2 == 0:
1217
yield DyckWord_class([ open_symbol for _ in range(self.k1) ])
1218
else:
1219
for w in DyckWordBacktracker(self.k1, self.k2):
1220
yield DyckWord_class(w)
1221
1222
def _an_element_(self):
1223
r"""
1224
TESTS::
1225
1226
sage: DyckWords(0).an_element() # indirect doctest
1227
[]
1228
sage: DyckWords(1).an_element() # indirect doctest
1229
[1, 0]
1230
sage: DyckWords(2).an_element() # indirect doctest
1231
[1, 0, 1, 0]
1232
"""
1233
return iter(self).next()
1234
1235
1236
def is_a_prefix(obj, k1 = None, k2 = None):
1237
r"""
1238
If ``k1`` is specified, then the object must have exactly ``k1`` open
1239
symbols. If ``k2`` is also specified, then obj must have exactly ``k2``
1240
close symbols.
1241
1242
EXAMPLES::
1243
1244
sage: from sage.combinat.dyck_word import is_a_prefix
1245
sage: is_a_prefix([1,1,0])
1246
True
1247
sage: is_a_prefix([0,1,0])
1248
False
1249
sage: is_a_prefix([1,1,0],2,1)
1250
True
1251
sage: is_a_prefix([1,1,0],1,1)
1252
False
1253
"""
1254
if k1 is not None and k2 is None:
1255
k2 = k1
1256
if k1 is not None and k1 < k2:
1257
raise ValueError, "k1 (= %s) must be >= k2 (= %s)"%(k1, k2)
1258
1259
1260
n_opens = 0
1261
n_closes = 0
1262
1263
for p in obj:
1264
if p == open_symbol:
1265
n_opens += 1
1266
elif p == close_symbol:
1267
n_closes += 1
1268
else:
1269
return False
1270
1271
if n_opens < n_closes:
1272
return False
1273
1274
if k1 is None and k2 is None:
1275
return True
1276
elif k2 is None:
1277
return n_opens == k1
1278
else:
1279
return n_opens == k1 and n_closes == k2
1280
1281
1282
def is_a(obj, k1 = None, k2 = None):
1283
r"""
1284
If k1 is specified, then the object must have exactly k1 open
1285
symbols. If k2 is also specified, then obj must have exactly k2
1286
close symbols.
1287
1288
EXAMPLES::
1289
1290
sage: from sage.combinat.dyck_word import is_a
1291
sage: is_a([1,1,0,0])
1292
True
1293
sage: is_a([1,0,1,0])
1294
True
1295
sage: is_a([1,1,0,0],2)
1296
True
1297
sage: is_a([1,1,0,0],3)
1298
False
1299
"""
1300
if k1 is not None and k2 is None:
1301
k2 = k1
1302
if k1 is not None and k1 < k2:
1303
raise ValueError, "k1 (= %s) must be >= k2 (= %s)"%(k1, k2)
1304
1305
1306
n_opens = 0
1307
n_closes = 0
1308
1309
for p in obj:
1310
if p == open_symbol:
1311
n_opens += 1
1312
elif p == close_symbol:
1313
n_closes += 1
1314
else:
1315
return False
1316
1317
if n_opens < n_closes:
1318
return False
1319
1320
if k1 is None and k2 is None:
1321
return n_opens == n_closes
1322
elif k2 is None:
1323
return n_opens == n_closes and n_opens == k1
1324
else:
1325
return n_opens == k1 and n_closes == k2
1326
1327
def from_noncrossing_partition(ncp):
1328
r"""
1329
Converts a non-crossing partition to a Dyck word.
1330
1331
TESTS::
1332
1333
sage: DyckWord(noncrossing_partition=[[1,2]]) # indirect doctest
1334
[1, 1, 0, 0]
1335
sage: DyckWord(noncrossing_partition=[[1],[2]])
1336
[1, 0, 1, 0]
1337
1338
::
1339
1340
sage: dws = DyckWords(5).list()
1341
sage: ncps = map( lambda x: x.to_noncrossing_partition(), dws)
1342
sage: dws2 = map( lambda x: DyckWord(noncrossing_partition=x), ncps)
1343
sage: dws == dws2
1344
True
1345
"""
1346
l = [ 0 ] * int( sum( [ len(v) for v in ncp ] ) )
1347
for v in ncp:
1348
l[v[-1]-1] = len(v)
1349
1350
res = []
1351
for i in l:
1352
res += [ open_symbol ] + [close_symbol]*int(i)
1353
return DyckWord(res)
1354
1355
def from_ordered_tree(tree):
1356
"""
1357
TESTS::
1358
1359
sage: sage.combinat.dyck_word.from_ordered_tree(1)
1360
Traceback (most recent call last):
1361
...
1362
NotImplementedError: TODO
1363
"""
1364
raise NotImplementedError, "TODO"
1365
1366
1367