Guide
User Manual: Pdf
Open the PDF directly: View PDF .
Page Count: 136
Download | |
Open PDF In Browser | View PDF |
Crash Course Coding Companion Samuel Hsiang Thomas Jefferson High School for Science and Technology samuel.c.hsiang@gmail.com Alexander Wei Phillips Exeter Academy Yang Liu Massachusetts Institute of Technology December 27, 2015 [copyright and license] https://www.dropbox.com/s/z2lur71042pjaet/guide.pdf?dl=0 https://github.com/alwayswimmin/cs_guide Acknowledgments iii Contents Acknowledgments iii Preface xiii 1 Fundamentals 1 1.1 Input and Output . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.2 Complexity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 1.3 First Problems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 1.3.1 Bessie’s Candles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 1.3.2 Froyo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 Sorting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 1.4.1 Insertion Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 1.4.2 Merge Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 1.4.3 Quicksort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 1.4.4 Ms. Manana’s Puzzles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 1.4 2 Big Ideas 2.1 2.2 7 Brute Force . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 2.1.1 Square Root . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 2.1.2 Combination Lock . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 2.1.3 Ski Course Design . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 2.1.4 Contest Practice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 Depth-First Search (DFS) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 2.2.1 Permutations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 2.2.2 Basketball . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 2.2.3 Problem Break . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 2.2.4 Generalizing DFS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 2.2.5 Dungeon . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 v Hsiang, Wei, Liu 2.2.6 2.3 Contents n Queens Puzzle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 Greedy Algorithms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 2.3.1 Bessie the Polyglot . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 2.3.2 More Cowbell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 2.3.3 Farmer John and Boxes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 2.3.4 Snack Time . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 3 Standard Library Data Structures 15 3.1 Generics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 3.2 List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 3.2.1 Dynamic Array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 3.2.2 Linked List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 3.3 Stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 3.4 Queue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 3.5 Heap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.6 Set . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 3.7 3.6.1 Binary Search Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 3.6.2 Hash Table . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 Map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 Graph Algorithms 4.1 4.2 4.3 4.4 21 31 35 Connected Components . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 4.1.1 Flood Fill . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 4.1.2 Union-Find (Disjoint Set Union) . . . . . . . . . . . . . . . . . . . . . . . . . 36 Shortest Path . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 4.2.1 Dijkstra . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 4.2.2 Floyd-Warshall . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2.3 Bellman-Ford . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 41 Minimum Spanning Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 4.3.1 Prim . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43 4.3.2 Kruskal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43 Eulerian Tour . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 vi Hsiang, Wei, Liu Contents 5 Complex Ideas and Data Structures 45 5.2 Dynamic Programming over Subsets (n2n DP) . . . . . . . . . . . . . . . . . . . . . 45 √ n Bucketing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 5.3 Segment Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 5.1 5.3.1 Lazy Propagation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 5.3.2 Fenwick Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 5.4 Queue with Minimum Query . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 5.5 Balanced Binary Search Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 5.5.1 Treap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 5.5.2 Tree Rotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64 5.5.3 Splay Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 5.5.4 Red-Black Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 6 Computational Geometry 75 6.1 Basic Tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 6.2 Formulas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 6.2.1 Area . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 6.2.2 Distance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 6.2.3 Configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 6.2.4 Intersection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 6.3 Convex Hull . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 6.4 Sweep Line . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 7 Tree Algorithms 79 7.1 DFS on Trees . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 7.2 Jump Pointers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80 7.3 Euler Tour Technique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 7.3.1 Euler Tour Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 7.4 Heavy-Light Decomposition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 7.5 Link-Cut Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 8 Strings 85 8.1 String Hashing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 8.2 Knuth-Morris-Pratt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 8.3 Trie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 8.4 Suffix Array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86 vii Hsiang, Wei, Liu Contents 8.5 Aho-Corasick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88 8.6 Advanced Suffix Data Structures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 8.6.1 Suffix Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 8.6.2 Suffix Automaton . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 9 More Graph Algorithms 93 9.1 Strongly Connected Components . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93 9.2 Network Flow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 9.2.1 Ford-Fulkerson . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97 9.2.2 Max-Flow Min-Cut Theorem . . . . . . . . . . . . . . . . . . . . . . . . . . . 98 9.2.3 Refinements of Ford-Fulkerson . . . . . . . . . . . . . . . . . . . . . . . . . . 99 9.2.4 Push-Relabel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100 9.2.5 Extensions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104 9.2.6 Bipartite Matchings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104 10 Math 105 10.1 Number Theory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 10.1.1 Random Prime Numbers Bounds . . . . . . . . . . . . . . . . . . . . . . . . . 105 10.1.2 Prime Number Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 10.1.3 Sieve of Eratosthenes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106 10.1.4 Prime Factorization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106 10.1.5 GCD and the Euclidean Algorithm . . . . . . . . . . . . . . . . . . . . . . . . 106 10.1.6 Fermat’s Little Theorem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106 10.1.7 Modular Inverses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106 10.2 Combinatorial Games . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106 10.3 Karatsuba . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 10.4 Matrices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 10.5 Fast Fourier Transform . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 11 Nonsense 11.1 Segment Tree Extensions 113 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 11.1.1 Fractional Cascading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 11.1.2 Persistence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 11.1.3 Higher Dimensions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 11.2 DP Optimizations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 11.3 Top Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 11.4 Link-Cut Cactus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 viii Hsiang, Wei, Liu Contents 12 Problems 115 12.1 Bronze . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115 12.2 Silver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115 12.2.1 Complete Search . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115 12.2.2 Greedy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115 12.2.3 Standard Dynamic Programming . . . . . . . . . . . . . . . . . . . . . . . . . 115 12.2.4 Standard Graph Theory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116 12.2.5 Easy Computational Geometry . . . . . . . . . . . . . . . . . . . . . . . . . . 119 12.3 Gold . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119 12.3.1 More Dynamic Programming . . . . . . . . . . . . . . . . . . . . . . . . . . . 119 12.3.2 Binary Search . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119 12.3.3 Segment Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121 12.3.4 More Standard Graph Theory . . . . . . . . . . . . . . . . . . . . . . . . . . . 121 12.3.5 Standard Computational Geometry . . . . . . . . . . . . . . . . . . . . . . . . 122 12.3.6 Less Standard Problems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 12.4 Beyond . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 12.4.1 Data Structure Nonsense . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 12.4.2 Other Nonsense . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 ix List of Algorithms 1 2 3 4 5 6 7 8 9 10 11 12 13 Union-Find . . . . . . . . . . . . . Dijkstra . . . . . . . . . . . . . . . Floyd-Warshall . . . . . . . . . . . Bellman-Ford . . . . . . . . . . . . Prim . . . . . . . . . . . . . . . . . Kruskal . . . . . . . . . . . . . . . Eulerian Tour . . . . . . . . . . . . Jump Pointers, Level Ancestor and Tarjan . . . . . . . . . . . . . . . . Ford-Fulkerson . . . . . . . . . . . Edmonds-Karp . . . . . . . . . . . Push-Relabel (Generic) . . . . . . Fast Fourier Transform . . . . . . . . . . . . . . . . . . . . . . . . . . . . LCA . . . . . . . . . . . . . . . xi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 39 42 42 43 44 44 81 95 97 99 103 108 Preface You might have heard of Evan Chen’s Napkin, a resource for olympiad math people that serves as a jumping point into higher mathematics.1 The Wikipedia articles on higher mathematics are just so dense in vocabulary and deter many smart young students from learning them before they are formally taught in a course in college. Evan’s Napkin aims to provide that background necessary to leap right in. I feel the same way about computer science. For most, the ease of the AP Computer Science test means that the AP coursework is often inadequate in teaching the simplest data structures, algorithms, and big ideas necessary to approach even silver USACO problems. On the other hand, even the best reference books, like Sedgewick, are too dense and unapproachable for someone who just wants to sit down and learn something interesting.2 The road, for many, stalls here until college. Everyone should be able to learn the simplest data structures in Java or C++ standard libraries, and someone with problem-solving experience can easily jump right into understanding algorithms and more advanced data structures. A few important notes, before we begin. • I’m assuming some fluency in C-style syntax. If this is your first time seeing code, please look somewhere else for now. • It is essential that you understand the motivations and the complexities behind everything we cover. I feel that this is not stressed at all in AP Computer Science and lost under the heavy details of rigorous published works. I’m avoiding what I call the heavy details because they don’t focus on the math behind the computer science and lose the bigger picture. My goal is for every mathematician or programmer, after working through this, to be able to code short scripts to solve problems. Once you understand how things work, you can then move on to those details which are necessary for building larger projects. The heavy details become meaningless as languages develop or become phased out. The math and ideas behind the data structures and algorithms will last a lifetime. • It is recommended actually code up each data structure with its most important functions or algorithm as you learn them. I truly believe the only way to build a solid foundation is to code. Do not become reliant on using the standard library (java.util, for instance) without understanding how the tool you are using works. 1 2 In fact, I’m using Evan’s template right now. Thanks Evan! Sedgewick, notably, is getting better. Check out his online companion to Algorithms, 4th Edition. xiii Chapter 1 Fundamentals 1.1 Input and Output The first part of solving any programming contest problem is reading the input correctly. In this section, we go over input and output in Java using java.util.Scanner and java.io.PrintWriter. There are two scenarios that you should be familiar with: stdin/stdout and file I/O. You encounter the former when you enter input and see output for a program run in the command line. You encounter the latter when you have two files (for example input.txt and output.txt) that you read from and write to. When using stdin/stdout, we read input from System.in using a Scanner and output our results using System.out.println(). Scanner.nextInt() and Scanner.next() read in integers and strings, respectively. System.out.println() prints its argument and adds a newline at the end. (If we don’t want the newline, we can use System.out.print().) Here’s an example of a main method that takes two integers and outputs their sum: 1 2 3 4 5 6 7 public static void main ( String args []) { // hint : you should write " import java . util .*;" at the top of your code . Scanner sc = new Scanner ( System . in ) ; int x = sc . nextInt () ; int y = sc . nextInt () ; System . out . println ( x + y ) ; } File I/O is a touch more complicated. For our Scanner, we have to make a new File object and use it in the constructor. We do the same for our PrintWriter. However, PrintWriter also comes with a couple more usage notes. First, we should include throws IOException after our main method, since Java requires that we acknowledge the possibility of an IOException. (We bravely assume that our file open will succeed!) After we finish printing, we must also close the PrintWriter to make sure that everything gets written. Here’s a snippet showing how Scanner and PrintWriter work with files: 1 Hsiang, Wei, Liu 1 2 3 4 5 6 7 8 9 Chapter 1. Fundamentals public static void main ( String args []) throws IOException { // hint : for file I /O , you should also have " import java . io .*;" Scanner sc = new Scanner ( new File ( " input . txt " ) ) ; int x = sc . nextInt () ; int y = sc . nextInt () ; PrintWriter pw = new PrintWriter ( new File ( " output . txt " ) ) ; pw . println ( x + y ) ; pw . close () ; } Although more efficient methods of I/O exist, such as BufferedReader and BufferedWriter, what’s covered here should be sufficient for now. (It’s possible to read in 105 integers with Scanner in a fraction of a second.) 1.2 Complexity Before we start computing contests, we should understand what the problems on these contests are trying to test. One class of problems, called implementation problems, assesses your ability write code quickly and accurately. These problems are only common in easier contests, since they usually don’t involve too much thinking or creativity—you just have to implement what’s written in the problem statement. Most competitive programming problems ask you to come up with a clever algorithm instead, testing speed and memory efficiency. To analyze the efficiency of algorithms, computer scientists use a concept called complexity. Complexity is measured as a function of the input size; an algorithm could require 3n, n4 /3 or even 2n + n2 steps to finish calculating for an input of size n. To express complexity, we use something called “big-O notation.” Essentially, we write the number of steps it takes for an algorithm to finish inside a pair of parentheses with an O in front, like this: O(# of steps). However, we drop any constant factors and lower order terms from the expression.1 I’ll explain why in a moment; let’s look at some examples for now. Suppose we have three programs that require 3n, n4 /3 and 2n + n2 steps to finish, respectively. The complexity of the first program is O(n) because we don’t care about the constant factor 3 on the 3n. The complexity of the second program is O(n4 ); again, we drop the constant. For the last program, we write its complexity as O(2n ) because n2 is a lower order term. As to why we drop the constants and the lower order terms, consider the first two programs from above. When n = 300, the first program takes 900 steps, while the second program takes 2, 700, 000, 000 steps. The second program is much slower, despite a smaller constant factor. Meanwhile, if we had a third program that runs in 5n steps, it would still only take 1, 500 steps to finish. Constant factors become pretty much irrelevant when we’re comparing functions that grow at different rates. The same can be said about lower order terms: n2 gets dwarfed by 2n even when n = 10. Thus in programming contests, we usually want a program with the correct complexity, without worrying about too much constant factors. Complexity will be the difference between whether a program gets accepted or time limit exceeded. As a rule of thumb, a modern processor can do 1 Actually, this isn’t entirely accurate. Saying an algorithm is O(f (n)) really means it takes at most c · f (n) steps to finish, for a sufficiently large constant c. 2 Hsiang, Wei, Liu Chapter 1. Fundamentals around 108 computations each second. When you plug the maximum possible n into the complexity of your algorithm, it should never be much more than that. We’ve focused on time and haven’t talked much about memory so far, but memory does gets tested. The amount of memory a program uses as a function of n is called its space complexity, as opposed to the time complexity we discussed earlier. Space complexity however, shows up much less frequently than time complexity—you usually run out of time before you can exceed the memory limit. 1.3 First Problems Now that you know the basics, let’s get started by solving a few problems. Try to figure out a solution to each problem before reading our analysis. To test your code for the problems in this chapter, you should create an account on Codeforces. After you login, there will be an “Introduction” problem set available here that includes these problems and a few extras. 1.3.1 Bessie’s Candles Bessie is a romantic cow; she spends her evenings writing code in the warm glow of candlelight. In her barn, she currently has a new candles, each of which burns for exactly one hour. Ever the efficient cow, Bessie can make a new candle from b burnt out candles. Your task, given a and b (2 ≤ a, b ≤ 1000), is to help her figure out the maximum number of hours for which she can keep her barn illuminated. [Adapted from Codeforces 379A.] Bessie’s Candles is a straightforward implementation problem. Since a and b are small, we can simulate Bessie’s candle burning process. We track of the number of candles we have left, the number of burnt out candles, and the number of hours that have passed. Whenever we have more than b burnt out candles, we make a new candle. Once we run out of candles, we print our answer. 1.3.2 Froyo Bessie has recently founded a froyo startup, offering froyo made from the best cows in Wisconsin. She has kept track of her profits for the first n days (1 ≤ n ≤ 105 ) and knows that she earned ai dollars on the i-th day. Growth is always important in startups, so Bessie wants to know the length of the longest non-decreasing subsegment in her profits. Help her figure this out! (Here, a subsegment denotes a contiguous block of numbers ai , ai+1 , · · · , aj (i < j).) [Adapted from Codeforces 580A.] Our first thought upon reading this problem might be to check each subsegment of the sequence ai and see if that subsegment is non-decreasing. Unfortunately, this is too slow. There are approximately n2 /2 pairs of endpoints that we could choose, far too many when n can be up to 105 . (Remember that rule of 108 ?) Instead, we can solve this problem with around n steps by performing a single sweep through the array. We maintain a counter representing Bessie’s currrent “streak”—the number of days since her profit last decreased. If her profit decreases from day i to day i + 1, then we reset the counter to zero. The answer we report is the longest streak that we ever see. 3 Hsiang, Wei, Liu Chapter 1. Fundamentals Froyo is a clear example of how getting the right complexity is essential. Our initial idea, which could have been implemented in O(n3 ) or O(n2 ), was too slow. To make our program finish in time, we had to work out a more efficient alrogithm that ran in O(n). 1.4 Sorting To further explore the concept of complexity, we will use sorting algorithms as a case study. Sorting is just as it sounds—we’re given a collection of objects, and we want to sort them into a predefined order. For example, suppose we have a list of scores from a programming contest. In order to generate the final standings, we’ll need to sort the contestants into ascending order by score. Below, we present three classic sorting algorithms of varying complexity: insertion sort, merge sort and quicksort. Don’t worry too much about the details of these algorithms for now. You’ll rarely need to implement them from scratch, since most modern programming languages come with built-in sorting algorithms. In our last subsection, we discuss how to use these library functions in Java and C++ with a problem that involves sorting as a subtask. To supplement our descriptions of the algorithms, you can check out the animations at http: //visualgo.net/sorting.html. 1.4.1 Insertion Sort Insertion sort builds up a sorted list by inserting new elements one at a time. Inserting an element into a sorted list takes time proportional to the length of the list, so the runtime of this algorithm is 1 + 2 + 3 + · · · + n = (n2 + n)/2, which is O(n2 ). One way to think about this is to iterate i from 1 to n, and let the first i elements be our sorted list. To insert the (i + 1)-th element, we just swap it with the largest, the second largest, and so on, until it’s greater than than the next largest element. Then we have a sorted list in the first (i + 1) entries of the array. Insertion sort, despite being slower than merge sort and quicksort, is still useful because of its efficiency on small inputs. Many implementations of merge sort and quicksort actually use insertion sort once the problem size gets small. Around how long is the longest list that you can sort with insertion sort in less than a second? 1.4.2 Merge Sort The idea behind merge sort is the following observation: If we are given two sorted lists of length n/2, we only need n comparisons to merge them into one sorted list of length n. It is easy to find the smallest element among the two lists, since this element has to be the smallest element in one of the lists. To find the second-smallest element, we can delete the smallest and do the same comparison again. This method of merging lists allows us to divide and conquer. We cut the array in half, sort each half recursively with merge sort, and then merge the two halves back together. Because our recursion goes log2 n levels deep and takes O(n) operations per level, this algorithm runs in O(n log n). (In fact, it is possible to prove that O(n log n) comparisons is optimal for sorting algorithms, one of the few problems in computer science that has a non-trivial lower bound.) 4 Hsiang, Wei, Liu Chapter 1. Fundamentals Around how long is the longest list that you can sort with merge sort in less than a second? 1.4.3 Quicksort Quicksort also uses a divide and conquer strategy to run in O(n log n) on average. We first take a random element from the array, called the pivot. We move anything less than the pivot to the left of the pivot and anything greater than the pivot to the right of the pivot. Like merge sort, we can then recursively quicksort the two “halves” that we just created. Since we choose the pivot randomly, our problem size usually gets cut down by a constant factor, giving us O(log n) levels of recursion with O(n) operations at each level. Thus quicksort runs in O(n log n) on average. We say “on average” because there are cases that can make quicksort run in O(n2 ). What would happen if we chose the smallest element of the array as the pivot each time? Quicksort was previously used by Java’s Arrays.sort and Collections.sort, but these functions now use dual-pivot quicksort and timsort. 1.4.4 Ms. Manana’s Puzzles The end of the school year is near and Ms. Manana will soon have to say goodbye to a yet another class. As a farewell present, she decides to give each of her n (1 ≤ n ≤ 50) students a jigsaw puzzle. The shop assistant tells Ms. Manana that there are m (1 ≤ m ≤ 50) puzzles in the shop, but they differ in difficulty and size. Specifically, the first jigsaw puzzle consists of f1 pieces, the second one consists of f2 pieces, and so on. Ms. Manana doesn’t want to upset the children, so she wants the difference between the numbers of pieces in her largest puzzle and her smallest puzzle to be as small as possible. Can you help Ms. Manana find this minimum difference? [Adapted from Codeforces 337A.] We solve this problem by first sorting the sequence fi . After sorting, Ms. Manana will want to buy puzzles from a contiguous block of the sequence. (If she doesn’t, then the difference between the largest and smallest puzzles will be greater than necessary.) Thus we can iterate through the sorted sequence to find the minimum difference between the endpoints of length n blocks. Usually, when solving a sorting problem, we don’t need to implement our own sorting function. If you’re using Java, java.utils.Arrays has a function Arrays.sort that does the magic for you. In C++, you can add #includeto your header and use std::sort. While coding Ms. Manana’s Puzzles, try to use the builtin-in sort function in your language. Here are code snippets for sorting an array arr of length n in Java and C++, respectively: 1 2 3 4 1 2 3 4 // hint : you should have " import java . util .*;" at the top of your code . int [] arr = new int [ n ]; // do something to fill up the array . Arrays . sort ( arr ) ; // hint : you should have "# include < algorithm >" at the top of your code . int arr [ n ]; // do something to fill up the array . std :: sort ( arr , arr + n ) ; 5 Chapter 2 Big Ideas In this chapter, we’ll discuss some general problem solving ideas: brute force, depth-first search, and the greedy algorithm. We can think of these as the building blocks to more complex methods—each provides a very general approach to simplifying problems. In programming contests, they also appear frequently by themselves as the core ideas to solutions. Since the concepts we cover are independent of language, we will no longer present algorithms in concrete Java or C++ code, but rather in more abstract pseudocode. 2.1 Brute Force Sometimes, the best way to approach a problem is to try everything. This idea of exhaustively searching all possibilities is called brute force. For example, if we want to unlock a friend’s iPhone, we could try all of the 104 possible passcodes. As the name and this example suggest, brute force is often crude and inefficient. Usually we want to make some clever observations to make the problem more tractable. However, if the input size is small (check the number of operations against 108 ) or if we want to squeeze a few points out of a problem by solving only the small cases, brute force could be the way to go. And if you’re stuck on a problem, thinking about a brute force is not a bad way to start. Simpler, slower algorithms can often inspire faster ones. Through the following problems, we’ll show you how to brutally apply the idea of brute force. 2.1.1 Square Root Given an integer n, 1 ≤ n ≤ 1012 , find the greatest integer less than or equal to √ n without using any library functions. (This means you can’t call functions like Math.sqrt or Math.log.) At first, it’s not obvious how we can compute square roots. However, we can always go simple. Set i = 1, and while (i + 1)2 ≤ n, increment i. That is, we increment i until increasing it further √ √ will cause i to exceed n. Since our answer i is at most n ≤ 106 , our program runs in time. This is about the silliest approach we can use to calculate square roots, but hey, it works! When implementing this algorithm, be careful about the size of n. The 32-bit int type in Java and C++ only holds values up to 231 − 1 = 2, 147, 483, 647, which is exceeded by the maximum 7 Hsiang, Wei, Liu Chapter 2. Big Ideas possible value of n. Thus we need to use a 64-bit integer type for our calculations: long in Java and long long in C++. 2.1.2 Combination Lock Farmer John purchases a combination lock to stop his cows from escaping their pasture and causing mischief! His lock has three circular dials, each with tick marks numbered 1 through N (1 ≤ N ≤ 100), with 1 and N adjacent. There are two combinations that open the lock: one combination set by Farmer John and one “master” combination set by the locksmith. The lock has a small tolerance for error, however, so it will open if the numbers on each of the dials are at most two positions away from that of a valid combination. Given Farmer John’s combination and the master combination, determine the number of distinct settings for the dials that will open the lock. (For example, if Farmer John’s combination is (1, 2, 3) and the master combination is (4, 5, 6), the lock will open if its dials are set to (1, N, 5) (since this is close to Farmer John’s combination) or to (2, 4, 8) (since this is close to the master combination). Note that (1, 5, 6) would not open the lock, since it is not close enough to any single combination. Furthermore, order matters, so (1, 2, 3) is distinct from (3, 2, 1).) [Adapted from USACO 2013, Combination Lock.] Again, the simplest idea works. We can iterate over all possible settings of the lock, and for each setting, check if it matches either Farmer John’s combination or the master combination. To do this, we can use three nested for loops. The first loop goes through the values for the first dial, the second loop through the values for the second dial, and the third loop through the values for the third dial. Since there are three dials, the lock has at most N 3 ≤ 106 possible settings. We can check if each dial matches in O(1) time, hence our algorithm runs in less than a second. In terms of implementation, Combination Lock is a great example of how a problem can decompose into two easier components that we can think about separately. The first component is to use nested loops to iterate through the possible settings, which we’ve described above. (Nested for loops like this show up often!) The second component is to check if a given setting is close to either of the given combinations. If we implement a function is_valid(a, b, c) to do this, then the code becomes quite clean. 2.1.3 Ski Course Design Farmer John has N hills on his farm (1 ≤ N ≤ 1000), each with an integer elevation in the range 0 to 100. In the winter, since there is abundant snow on these hills, he routinely operates a ski training camp. In order to evade taxes, Farmer John wants to add or subtract height from each of his hills so that the difference between the heights of his shortest and tallest hills is at most 17 before this year’s camp. Suppose it costs x2 dollars for Farmer John to change the height of a hill by x units. Given the current heights of his hills, what is the minimum amount that Farmer John will need to pay? (Farmer John is only willing to change the height of each hill by an integer amount.) [Adapted from USACO 2014, Ski Course Design.] 8 Hsiang, Wei, Liu Chapter 2. Big Ideas For Ski Course Design, we need to be a bit clever about how to implement our brute force. There are infinitely many ways we could change the heights of each hill, so it seems intractable to iterate over the possible heights for each hill separately. Instead, we look at the final range of the ski slope heights, which has length at most 17. This final range has to fall within the interval [0, 100], hence there are less than 100 possibilities. (The possible ranges are [0, 17], [1, 18], · · · , [83, 100].) Once we fix a range, we can calculate in O(N ) the minimum cost to make the height of each hill fall within that range. Thus if we let M be the number of possible ranges (M < 100), we have an O(M N ) algorithm. This problem shows that even when we brute force, we still have to think. Some approaches are better than others. In particular, we don’t want to deal with cases that are irrelevant—for example, when the heights are not within a range of width 17 or when Farmer John has not used the cheapest set of changes. We also don’t want our possibilities to explode out of control, which would have happened had we adjusted the height of each hill separately with nested for loops or recursion. By iterating over a more restricted set of possibilities, we have created a brute force solution that runs significantly faster. 2.1.4 Contest Practice Here is a collection of problems solvable through brute force. Try working through them on your own and applying the ideas you’ve seen so far. (May the brute force be with you.) 2.2 Depth-First Search (DFS) Depth-first search is a recursive search technique that provides us with another way to brute force. Instead of using nested loops to iterate through all possibilities, we can generate the possibilities using recursion, checking all possible choices in each level. Here’s an example: All of your friends, upon realizing that it only takes four nested for loops to iterate through all possible 4-digit iPhone passcodes, have decided to make their passcodes n digits long. Since you don’t know n, nested for loops will no longer do the trick. Instead, we can use a DFS to recursively generate all n-digit passcodes. Depth-first search works as follows: We check all passcodes starting with “0”, then all passcodes starting with “1”, then all passcodes starting with “2”, and so on. To check all passcodes starting with “0”, we check all passcodes starting with “00”, then all passcodes starting with “01”, then all passcodes starting with “02”, and so on. To check all passcodes starting with “00”, we have to check all passcodes starting with “000”, then all passcodes starting with “001” and so on... (Think about why DFS is depth-first.) In this way, we recursively generate all possible passcodes by extending the prefix character by character. We keep recursing until we have to check a passcode starting with a string of length n, in which case that string is the passcode itself. Thus the first passcode we check is “00· · · 0” and the last passcode we check is “99· · · 9”. We implement this algorithm by writing a function that generates all passcodes given a prefix. Below is some pseudocode describing the algorithm. Make sure you understand how the function calls itself! 9 Hsiang, Wei, Liu 1: 2: 3: 4: 5: 6: Chapter 2. Big Ideas function generatePasscodes(depth, pref ix) if depth = n then . If we’ve reached maximum depth, then print and return. print(pref ix) return for c from ‘0’ to ‘9’ do . Iterates over all possible next digits. generatePasscodes(depth + 1, pref ix + c) . Recurses with a longer prefix. 2.2.1 Permutations Given n (n ≤ 8), print all permutations of the sequence {1, 2, · · · , n} in lexicographic (alphabetical) order. (For n = 3, this would be (1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), and (3, 2, 1).) Like the passcode problem, we use DFS instead of nested for loops, since we don’t know n. However, we have to be careful with implementation—we can use each number only once. Along with our current prefix, we have to keep track of the set of numbers that we’ve already used. This is best done with a Boolean “used” array outside of the recursive function. Here’s the pseudocode: 1: used ← {f alse, f alse, · · · , f alse} . Initialize used as an array of f alse values. 2: function generatePermutations(depth, pref ix) 3: if depth = n then 4: print(pref ix) 5: return 6: for i = 1 to n do 7: if not used[i] then 8: used[i] ← true 9: generatePermutations(depth + 1, pref ix + i) 10: used[i] ← f alse . We have to reset the used[i] variable once we’re done. To understand the order in which we visit the permutations, we can visualize this algorithm as traversing a tree-like structure. An animation of this algorithm for n = 5 is here. 2.2.2 Basketball Two teams are competing in a game of basketball: the Exonians and the Smurfs. There are n players on the Exonian team and m players on the Smurf team, with n + m ≤ 17. Each player has an integer skill level s between 1 and 108 . Define the strength of a set of players as the sum of their individual skill levels. In order to ensure a fair game, the Exonians and Smurfs plan on choosing two equally strong starting lineups. In how many ways can the two teams choose their lineups? (Two lineups are considered different if there exists a player who starts in one game, but not in the other.) We use a DFS to recursively generate all possible starting lineups. Each starting lineup can be represented by a sequence of n + m 0’s and 1’s, where a player starts if and only if he/she is assigned a 1. We do this the same way we generate all passcodes of length n + m. Once we have a starting lineup, it is straightforward to check for fairness. (Is it also possible to keep track of the strength of each team as we DFS? Hint: Keep an extra variable similar to “used” in Permutations.) 10 Hsiang, Wei, Liu 2.2.3 Chapter 2. Big Ideas Problem Break Before moving on, try to implement the DFS problems described above. You can test your code on the problem set here. Try to do some complexity analysis too. How does the runtime of these algorithms grow with respect to n? 2.2.4 Generalizing DFS Thus far, all of the DFS solutions that we’ve seen have involved sequences. However, we can also use DFS in a much more general setting. In the same spirit as brute force, if we want to enumerate or construct something and we have a number of options at each step, we can recurse on each of the possible options and thereby check all possibilities. The examples below show the flexibility of depth-first search—a huge class of problems can be solved in a brute force manner like this. 2.2.5 Dungeon Bessie is trying to escape from the dungeon of the meat packing plant! The dungeon is represented by an n-by-n grid (2 ≤ n ≤ 6) where each of the grid cells is trapped and can only be stepped on once. Some cells of the grid also contain obstacles that block her way. Bessie is currently located in the upper-left corner and wants to make her way to the exit in the lower-right corner. How many paths can Bessie take to escape, assuming that she avoids all obstacles and steps on no cell twice? We write a function DFS(x, y) thats runs a DFS from cell (x, y) and counts the number of paths to the lower-right corner given obstacles and previously visited squares. Upon arriving at (x, y), we mark that cell as visited. If (x, y) is the destination, we increment our answer by one. Otherwise, we try recursing in each direction—up, down, left, and right. Before recursing, we check that we don’t go out of bounds and that we don’t step on an obstacle or previously visited square. Once we’re done counting paths from (x, y), we have to remember to mark this cell as unvisited again. In terms of implementation, it is easiest to store the obstacles and visited cells in Boolean arrays outside of the recursive function. We can also define a function ok(x, y) to check if a cell (x, y) is safe to step on—if x and y are in bounds, (x, y) is not an obstacle, and (x, y) is unvisited. Finally, to avoid doing four cases, one for each direction, we can define two arrays dx and dy which contain [1, 0, -1, 0] and [0, 1, 0, -1], respectively. Then dx[i] and dy[i] for 0 ≤ i < 4 represent the changes in x- and y-coordinates for each of Bessie’s possible moves. 2.2.6 n Queens Puzzle Given n (4 ≤ n ≤ 8), find an arrangement of n queens on an n-by-n chessboard so that no two queens attack each other. First, observe that each row must contain exactly one queen. Thus we can assume that the i-th queen is placed in the i-th row. Like before, we try putting each queen on each possible square in its row and recurse, keeping track of used columns and diagonals. When we add the i-th queen, we mark its column and diagonals (both of them) as attacked. As usual, once we’re done checking all possibilities for the i-th queen, we unmark its column and diagonals. We terminate when we find an arrangement using all n queens. 11 Hsiang, Wei, Liu 2.3 Chapter 2. Big Ideas Greedy Algorithms A greedy algorithm is an algorithm that makes the (locally) most desirable choice at each step. Instead of checking all possibilities as we do in a depth-first search, we choose the option that “seems” best at the time. This usually results in a fast and easy to implement algorithm. While this may not be a good way to live life—doing homework would always be relegated in favor of solving programming contest problems—there exist many computer science problems where being greedy produces optimal or almost optimal results. Keep in mind, however, that greedy algorithms usually don’t work. Before coding any greedy solution, you should be able to convince yourself with a proof of correctness. Let’s go through a few examples to get a better sense for what greedy algorithms are like. 2.3.1 Bessie the Polyglot Bessie would like to learn how to code! There are n (1 ≤ n ≤ 100) programming languages suitable for cows and the i-th of them takes ai (1 ≤ ai ≤ 100) days to learn. Help Bessie determine the maximum number of languages she could know after k (1 ≤ k ≤ 104 ) days. [Adapted from Codeforces 507A.] It’s pretty easy to see that we want to learn programming languages in order of increasing ai , starting from the smallest. Thus Bessie can obtain an optimal solution with a greedy approach. She first learns the language that requires the fewest days to learn, then the language that takes the next fewest days, and so on, until she can’t learn another language without exceeding k days. To implement this, we sort ai and find the longest prefix whose sum is at most k. Due to sorting, the complexity is O(n log n). With greedy algorithms, we usually want to have an ordering or heuristic by which we decide what is the best choice at a given instance. In this case, our ordering was the most straightforward possible, just choosing languages in order of increasing learning time. However, figuring out how we want to greedily make decisions is not always obvious. Oftentimes, coming up with the correct heuristic is what makes greedy algorithms hard to find. 2.3.2 More Cowbell Kevin Sun wants to move his precious collection of n (1 ≤ n ≤ 105 ) cowbells from Naperthrill to Exeter, where there is actually grass instead of corn. Before moving, he must pack his cowbells into k boxes of a fixed size. In order to keep his collection safe during transportation, he will not place more than two cowbells into a single box. Since Kevin wishes to minimize expenses, he is curious about the smallest size box he can use to pack his entire collection. Kevin is a meticulous cowbell collector and knows that the size of his i-th (1 ≤ i ≤ n) cowbell is an integer si . In fact, he keeps his cowbells sorted by size, so si − 1 ≤ si for any i > 1. Also an expert packer, Kevin can fit one or two cowbells into a box of size s if and only if the sum of their sizes does not exceed s. Given this information, help Kevin determine the smallest s for which it is possible to put all of his cowbells into k (n ≤ 2k ≤ 105 ) boxes of size s. [Adapted from Codeforces 604B.] 12 Hsiang, Wei, Liu Chapter 2. Big Ideas Intuitively, we want to use as many boxes as we can and put the largest cowbells by themselves. Then, we want to pair the leftover cowbells so that the largest sum of a pair is minimized. This leads to the following greedy algorithm: First, if k ≥ n, then each cowbell can go into its own box, so our answer is max(s1 , s2 , · · · , sn ). Otherwise, we can have at most 2k − n boxes that contain one cowbell. Thus we put the 2k − n largest cowbells into their own boxes. For the remaining n − (2k − n) = 2(n − k) cowbells, we pair the ith largest cowbell with the (2(n − k) − i + 1)-th largest. In other words, we match the smallest remaining cowbell with the largest, the second smallest with the second largest, and so on. Given these pairings, we can loop through them to find the largest box we’ll need. The complexity of this algorithm is O(n) since we need to iterate through all of the cowbells. To prove that this greedy algorithm works, we first prove the case n = 2k, where each box must contain exactly two cowbells. Consider any optimal pairing of cowbells. If s2k is not paired with s1 , then we can perform a swap so that they are paired without increasing the size of the largest box: Suppose we initially have the pairs (s1 , si ) and (s2k , sj ). If we rearrange them so that we have the pairs (s1 , s2k ) and (si , sj ), then s1 + s2k , si + sj ≤ s2k + sj . After we’ve paired the largest cowbell with the smallest, we can apply the same logic to the second largest, third largest, and so on until we’re done. Therefore, our construction is optimal if n = 2k. For n < 2k, we can imagine that we have 2k − n cowbells of size 0 and use the same argument. This method of proving correctness for a greedy algorithm is rather common. We want to show that our greedily constructed solution is as good as an arbitrarily chosen optimal solution, so we compare where the optimal solution and our greedy solution differ. Once we find a difference, we try to transform one to the other without changing the value we’re trying to optimize. In this case, since transforming the optimal solution to our greedily constructed solution doesn’t make it worse, our solution must be optimal as well. 2.3.3 Farmer John and Boxes Farmer John has n (1 ≤ n ≤ 100) boxes in his barn. His boxes all have the same size and weight but have varying strengths–-the i-th box is able to support at most xi other boxes on top of it. If Farmer John wants to stack his boxes so that each box is directly on top of at most one other box, what is the minimum number of stacks that he’ll need? [Adapted from Codeforces 388A.] We first observe that if a stronger box is on top of a weaker box, then we can swap the two boxes and still have a valid stacking. Therefore, an optimal solution exists where no box is stronger than any box below it. This allows us to sort the boxes in increasing order of strength and construct an optimal solution by inserting stronger boxes below piles of weaker boxes. Initially, we start with a single empty stack. We then add boxes one-by-one as follows: If we can, we insert our new box below some stack that it can successfully support. Otherwise, if no such stack exists, we create a new stack containing only the new box. It’s possible to check all existing stacks in O(n); therefore this algorithm runs in O(n2 ). (In fact, binary searching allows us to do the checking step in O(log n).) To prove correctness, we can again compare our construction to an arbitrary optimal construction. Finishing the argument is left as an exercise for the reader. The subtle part about this problem is the order in which we process the boxes. Processing boxes from weakest to strongest offers an elegant greedy solution, while processing from strongest to 13 Hsiang, Wei, Liu Chapter 2. Big Ideas weakest offers no simple approach. (At least I haven’t found one yet.) Again, we see that ordering is important. Stepping back a bit, this problem also isn’t one that immediately suggests a greedy approach. Since greedy solutions can often be unexpected, it’s always worthwhile to take a moment to consider various orderings and “naïve” approaches to see if any work. Telltale hints of a greedy approach are observations about order and monotonicity—for example “we can always have stronger boxes below weaker ones.” 2.3.4 Snack Time Here’s the link to the greedy problem set. Feast away! 14 Chapter 3 Standard Library Data Structures The purpose of this chapter is to provide an overview on how the most basic and useful data structures work. The implementations of most higher-level languages already coded these for us, but it is important to know how each data structure works rather than blindly use the standard library. More technical explanations of all of these can be found in a language’s API. For Java, this is mostly under the package java.util, in the Java API. I strongly believe that Java is better than C++ for beginning programmers. It forces people into good coding habits, and though the lack of pointers initially frustrated me, it really does make learning general concepts liked LinkedLists much easier, as the intricacies of the C++ pointer no longer distract from the larger idea. 3.1 Generics In general, a data structure can store any kind of data, ranging from integers to strings to other data structures. We therefore want to implement data structures that can hold any and all kinds of information. When we use a data structure, however, we might want our structure to store only one kind of information: only strings, for example, or only integers. We use generics to specify to an external structure that we only want it to store a particular kind of information. 1 ArrayList < Integer > al = new ArrayList < Integer >() ; This means that al is an ArrayList of Integers. We can only add Integers into the ArrayList, and anything removed from the ArrayList is guaranteed to be an Integer. We can write Integer i = al.get(0) without any need to cast to an Integer. I don’t think the beginning programmer needs to know how to necessarily code a class that supports generics, since each language has its own complex set of rules governing generics. However, we use the standard library extensively in any coding environment, so it is necessary to use a class that does support generics. I think standard classes are relatively straightforward to use but can be annoying to actually implement. When examining Java API or explanations of implemented functions in this chapter, the characters E, V, and K all can represent generics. For C++, generics are denoted by strings like 15 Hsiang, Wei, Liu Chapter 3. Standard Library Data Structures value_type. For example, in Java, when we set al = new ArrayList (), E represents Integer. Otherwise, E simply means any object. 3.2 List A list is a collection of objects with an ordering. The objects are ordered in the sense that each element is associated with an index that represents its placement in the list. Users of a list have control over where in the list each object is and can access a specific element by its index, like in an array. 3.2.1 Dynamic Array What is nice about an array? We can access or change any element we want in the array in O(1) time. The problem is that an array has fixed length. It’s not easy to append an element to the end of an array. The fix to this is pretty simple. Why not just make a bigger array, and copy everything over to the new array? Then there’s more room at the end to add a new element. If the backbone array runs out of space, we create a new array with double the size and keep going as if nothing happened. Therefore we now have an array of extendable size – a dynamic array. "a" "b" null 0 1 2 "a" "b" "c" 0 1 2 "a" "b" "c" null null null 0 1 2 3 "a" "b" "c" "d" 0 1 2 3 4 5 null null 4 5 We see that there is still room in the array to add "c", but to add more elements to the list, we must use a new array with double the length. It’s important to note that any given insertion to the structure is either O(n) or O(1), but there is only one O(n) insertion for every O(n) O(1) insertions, so we still average out to constant time. The Java implementation of a dynamic array is the ArrayList. The C++ implementation is the vector. For the following operations, think about how you would implement each and analyze its time complexity. 16 Hsiang, Wei, Liu Chapter 3. Standard Library Data Structures Function Java, ArrayList C++, vector add an element to the end of the list boolean add(E e) void push_back(const value_type& val)1 insert an element to a partic- void add(int index, E ele- iterator insert(iterator position, const ular index in the list, shifting ment) 2 value_type& val) all subsequent elements down one index access the element stored at a particular index E get(int index) update the value of the element stored at a particular index to a new element E set(int index, E ele- reference ment) (size_type n) search whether the list con- boolean o) tains a particular element contains(Object reference operator[] 3 (size_type n) operator[] iterator find (iterator first, iterator last, const value_type& val)4 remove the element at a par- E remove(int index) ticular index from the list iterator erase (iterator position) search for and remove a given element from the list boolean remove(Object o) use iterators return the size of the list5 int size() size_type size() const Accessing and updating elements at particular indices are very nice. They are easy to code and run in constant time. These are the bread and butter of any array. Adding at the end of the list is nice as well. Checking whether some element is contained in the list is a pain, as it is O(n), and adding to and removing from early in the list are more annoying. 3.2.2 Linked List Arrays are nice for accessing, say, the seventh element in the list. We extend this to the dynamic array to implement adding and removing elements to and from the end of the list nicely. Removing elements from the beginning of the list, however, is cumbersome. The linked list attempts to remedy this. It trades O(1) access to any element in the list for an easier way to remove elements from either end of the list easily. Consider a chain of paper clips: 1 & is a C++ reference An iterator is like a pointer that allows for traversal of a data structure in a particular order. For example, we can increment the iterator to access the next element in the list. An iterator is NOT merely an integer representing the relative position in our list, as the in the Java implementation of add 3 size_type is, for our purposes, unsigned int, and reference operator[] makes this structure’s syntax more like a normal array. In particular, to access the element at index i of a vector v, we simply use v[i], and to update an element, we use v[i] = val. 4 This function is in . 5 Note that the length of the list is not simply the size of the backing array. 2 17 Hsiang, Wei, Liu Chapter 3. Standard Library Data Structures 6 It’s easy to add or remove more paper clips from either end of the chain, and from any given paper clip, it’s easy to access the paper clip directly previous or next to it in the chain. If we needed the seventh paper clip in the chain, we’d need to manually count, an O(n) operation. However, if we then needed to remove that paper clip from the chain, it wouldn’t be that hard, assuming we kept a finger, or pointer, on the seventh paper clip. The best way to think about and implement a linked list is through a cyclical doubly-linked list, with a dummy head. This means each element has its own node container, while the head of the list is simply a node without an element. Such a data structure looks something like this: null "a" "b" "c" We see that each node maintains a pointer to its next neighbor and its previous neighbor, in addition to containing the String it stores. We can store this data in a class like the following: 1 2 3 4 class ListNode { ListNode prev , next ; E s; } If we were to insert an element after a ListNode a, it is necessary to update all pointers: 1 2 3 4 5 ListNode < String > b = new ListNode < String >() ; b . prev = a ; b . next = a . next ; b . next . prev = b ; a . next = b ; 6 http://img.thrfun.com/img/078/156/paper_clip_chain_s1.jpg 18 Hsiang, Wei, Liu Chapter 3. Standard Library Data Structures Since the linked list is symmetric, inserting an element before a node is also easy. To add something to the end of the list, simply add it before the dummy head. From here it should not be too hard to implement all the important functions of a linked list. The Java implementation of a linked list is LinkedList, and the C++ implementation is list. A second C++ class that performs the same tasks but uses a backing array instead of a linked list structure is the deque. Function Java, LinkedList C++, list add an element to the end boolean add(E e) insert 7 void add(int index, E element) iterator insert(iterator position, const value_type& val) access E get(int index) use iterators reference operator[] (size_type n) update E set(int index, E element) use iterators reference operator[] (size_type n) search boolean contains(Object o) void push_back(const value_type& val) iterator find (iterator first, iterator last, const value_type& val)8 remove the ele- E remove(int index) ment at a particular index search for and re- boolean move a given ele- move(Object o) ment C++, deque iterator erase (iterator position) re- void remove (const value_type& val) use iterators size int size() end operations addFirst, addLast, push_front, push_back, pop_front, pop_back getFirst, getLast, removeFirst, removeLast size_type size() const With a linked list implemented, two other data structures immediately follow. 3.3 Stack A stack gets its name from being exactly that: a stack. If we have a stack of papers, we can push things on the top and pop things off the top. Sometimes we peek at to access the element on top 7 8 The index in a linked list is implicit. This function is in . 19 Hsiang, Wei, Liu Chapter 3. Standard Library Data Structures but don’t actually remove anything. We never do anything with what’s on the bottom. This is called LIFO: Last In, First Out. Java implements the stack with Stack, C++ with stack. Function Java, Stack C++, stack push E push(E item) void push value_type& val) pop E poll() void pop() top E peek() value_type& top() size int size() size_type size() const (const Java implements Stack using an array-like structure. This works just as well, and is faster in practice, but I prefer the linked-list structure as a mathematical concept as it is more elegant in its relationship with the queue and more easily customizable. 3.4 Queue A queue is like a queue waiting in line for lunch. We push to the end and pop from the front. Sometimes we peek at the front but don’t actually remove anything. The first person in line gets served first. This is called FIFO: First In, First Out. In Java, Queue is an interface, and in C++, the implementation of the queue is queue. Function Java, Queue C++, queue push boolean offer(E e) void push value_type& val) pop E poll() void pop() top E peek() value_type& front() size int size() size_type size() const (const Since Queue is an interface in Java, we cannot instantiate a Queue, so the following statement is illegal. 1 Queue < String > q = new Queue < String >() ; Instead, we must use LinkedList, so we do something like this: 1 Queue < String > q = new LinkedList < String >() ; This is legal because LinkedList implements Queue, making it the standard implementation of the FIFO queue. 20 Hsiang, Wei, Liu 3.5 Chapter 3. Standard Library Data Structures Heap Quite often a FIFO queue is not always desirable. For example, perhaps the string I want to remove at every given point is the one that is lexicographically least. A min heap is a tree such that every node is smaller than or equal to all of its children. A max heap is a tree such that every node is larger than or equal to all of its children. Pictured is a complete binary min heap, which will be of use to us. "a" "b" "c" "e" "d" "g" "n" "m" "p" "o" "i" "h" "l" "j" "k" "f" We see that the root of the tree will always be the smallest element. It is tempting to use a container class with a pointer to its left and its right child. However, we have a much nicer way to store complete binary trees with an array. Consider the following numbering of the nodes: 1 "a" 2 "b" 3 "d" 4 "c" 8 "e" 5 "n" 9 "m" 10 "p" 6 "g" 11 "o" 12 "i" 7 "h" 13 "l" 14 "k" 15 "j" 16 "f" We see that every number from 1 to 16 is used, and for every node, if the index associated with it is i, the left child is 2i, and the right child is 2i + 1. This leads to a very natural implementation of the tree in an array: 21 Hsiang, Wei, Liu Chapter 3. Standard Library Data Structures null "a" "b" "d" "c" "n" "g" "h" "e" "m" "p" "o" "i" "l" "k" "j" "f" 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 How do we add elements to our heap, while maintaining the heap qualities? Well, let’s just add it to the very end and see what we get. Suppose we are to add "b" to the tree. 1 "a" 2 "b" 3 "d" 4 "c" 8 "e" 16 "f" 5 "n" 9 "m" 10 "p" 6 "g" 11 "o" 12 "i" 7 "h" 13 "l" 14 "k" 15 "j" 17 "b" Well, "b" comes before "e" in the alphabet, so let’s swap the nodes. We are guaranteed that "b" should come before the other child (in this case, "f") by the transitive property. 1 "a" 2 "b" 3 "d" 4 "c" 8 "b" 16 "f" 5 "n" 9 "m" 10 "p" 6 "g" 11 "o" 12 "i" 17 "e" One more swap... 22 7 "h" 13 "l" 14 "k" 15 "j" Hsiang, Wei, Liu Chapter 3. Standard Library Data Structures 1 "a" 2 "b" 3 "d" 4 "b" 8 "c" 16 "f" 5 "n" 9 "m" 10 "p" 6 "g" 11 "o" 12 "i" 7 "h" 13 "l" 14 "k" 15 "j" 17 "e" And now we have the heap property restored. As the tree has depth at most log n, this process is O(log n). To remove the root from the heap, we replace the root with the last leaf: 1 "e" 17 "a" 2 "b" 3 "d" 4 "b" 8 "c" 5 "n" 9 "m" 10 "p" 6 "g" 11 "o" 12 "i" 7 "h" 13 "l" 14 "k" 15 "j" 16 "f" We perform a series of swaps to restore the heap property. We always want to choose the smaller child to swap until the heap property is satisfied. 23 Hsiang, Wei, Liu Chapter 3. Standard Library Data Structures 1 "b" 17 "a" 2 "e" 3 "d" 4 "b" 8 "c" 5 "n" 9 "m" 10 "p" 6 "g" 11 "o" 12 "i" 7 "h" 13 "l" 14 "k" 15 "j" 16 "f" 1 "b" 17 "a" 2 "b" 3 "d" 4 "e" 8 "c" 5 "n" 9 "m" 10 "p" 6 "g" 11 "o" 12 "i" 7 "h" 13 "l" 14 "k" 15 "j" 16 "f" 1 "b" 17 "a" 2 "b" 3 "d" 4 "c" 8 "e" 5 "n" 9 "m" 10 "p" 6 "g" 11 "o" 12 "i" 7 "h" 13 "l" 14 "k" 15 "j" 16 "f" And we are done. Once again, this takes at most log(N ) swaps. This idea can be extended to removing or changing the value of any node we’d like from a tree – this is particularly useful for Dijkstra later. 24 Hsiang, Wei, Liu Chapter 3. Standard Library Data Structures Remember to implement your heap in an array-like structure! Java implements a min heap with the PriorityQueue. This class, like LinkedList, also implements Queue. C++ implements a max 9 heap with the priority_queue. The functions for heaps in both languages are nearly identical to those for queues. Function Java, PriorityQueue C++, priority_queue push boolean offer(E e) void push value_type& val) pop E poll() void pop() top E peek() value_type& top() size int size() size_type size() const 3.6 (const Set A set is a collection of objects with no duplicate elements. Note that the data structures discussed in this section can be extended to become multisets, but Java and C++ implementations of these explicitly disallow multiplicity. 3.6.1 Binary Search Tree A binary search tree (BST) is a tree where every node is greater than every node in its left subtree and less than every node in its right subtree. As with a heap, to use a BST, we need to impose some kind of ordering on the elements stored. "m" "g" "j" "c" "b" "t" "e" "h" "k" "a" 9 "r" Don’t forget that C++ implements a max heap, ever. 25 "s" Hsiang, Wei, Liu Chapter 3. Standard Library Data Structures The tree need not be complete, unlike the heap. Because it is not guaranteed to be complete, there is no way to nicely bound the size of the array we would need if we were to use the same storage method as with the heap. Thus, we are forced to use a TreeNode, with left and right pointers. This is also problematic when determining guarantees on time complexities later, but the ways to solve this problem are pretty complicated so we’ll ignore them for now. Given the name of the tree, searching for an element within the tree is quite natural, and similar to a binary search. Compare the element to be searched for with the current node. If they are equal, we are done; otherwise, search the appropriate left or right subtree. As with most structures and algorithms with a binary search structure, this operation lends itself nicely to recursion. If the tree is reasonably nice, we expect to complete this in O(log n) time, but searching can be as bad as linear if the tree looks like a linked list. Adding an element is also natural. As our tree represents a set, it will not contain the same element twice. We trace down until we hit a null pointer, and add the element in the appropriate spot. Let’s add a "p" to the BST: "m" "g" "j" "c" "b" "t" "e" "h" "r" "k" "p" "s" "a" Deleting an element is the annoying part. Unfortunately, there’s not much we can do besides casework. Removing a leaf, like "a", from the tree is very easy. Removing a node with only once child, like "t", is also relatively straightforward. 26 Hsiang, Wei, Liu Chapter 3. Standard Library Data Structures "m" "g" "t" "j" "c" "b" "e" "h" "r" "k" "p" "s" "a" Now, removing an element with two children is tricky. We’ll try to remove "g". Consider the least element in the right subtree of "g", which in this case is "h". We find "h" by always choosing the left child on the right subtree until we cannot go any further. This must be the least element. "m" "g" "j" "c" "b" "r" "e" "h" "p" "s" "k" Note that "h" has either no children or only one child, and that nodes like these are easy to remove. We then change the value of the node containing "g" to "h", which is legal since "h" is the least element, and remove "h" from the right subtree, and we are done. "m" "h" "j" "c" "b" "r" "e" "h" "p" "k" 27 "s" Hsiang, Wei, Liu Chapter 3. Standard Library Data Structures "m" "h" "j" "c" "b" "r" "p" "e" "s" "k" Since a BST is ordered, iterating over it from left to right will pass over every element in sorted order. A standard BST has O(log n) operations if the tree is “nice,” or sufficiently randomized, but each operation can be O(n) in the worst case. We need to find a way to automatically balance the BST such that we avoid linear time complexities. A red-black tree is a self-balancing BST that guarantees O(log n) operations by making sure the height of the tree grows logarithmically. It is implemented in Java’s TreeSet and is usually10 implemented in the C++ set, so while the simple BST I described above does not guarantee nice time bounds, Java’s implementation does. I don’t think learning exactly how a red-black tree works is particularly useful for the beginning programmer or a competitive programmer. How exactly a red-black tree works, together with some more balanced binary search trees which are useful on the competitive scene, are covered in a later chapter. Function Java, TreeSet C++, set insert boolean add(E e) void insert value_type& val) search boolean o) size_type count (const value_type& val) const11 delete boolean remove(Object o) size_type erase (const value_type& val), void erase (iterator position) size int size() size_type size() const other useful search functions first, last, ceiling, floor, begin, end, lower_bound12 , higher, lower upper_bound13 , find contains(Object 10 (const Implementations of set must be some kind of balanced binary search tree, but need not be red-black. 1 if in the set, 0 otherwise. 12 lower_bound is similar to Java’s ceiling 13 upper_bound is similar to Java’s higher 11 28 Hsiang, Wei, Liu 3.6.2 Chapter 3. Standard Library Data Structures Hash Table The fastest way to store values in a collection is through an array structure. Unfortunately, searching an array for a specific value is very costly. The binary search tree provided a quick way to search for a value by imposing constraints on its placement based on characteristics of the value. Similarly, to find a specific value in an array, we need to impose constraints on what index the value is placed in based on the value itself. Suppose, for instance, I wanted to store some set of numbers from 0 to 999. I want to quickly add a new number to our set, remove a number, or check if a number is in our set. The easiest way to do this is to maintain a size-1000 array of boolean values. If a number i is in our set, we set the ith index in our array to true. We then have O(1) updates and queries for our set. To extend this to other values, we define the hash function. The hash function operates on the object and returns something that characterizes that object. For example, for a string, a possible hash could be the length of the string or the sum of the characters. We want to map an object with an integer hash, so that we can store the values by their hashes in an array. The resulting structure is a hash table. What characterizes a good hash function? 1. If two objects are considered equal, like the strings "Hello" and "Hello", their hashes must be equal. 2. If two objects are not equal, like the strings "Hello" and "Bye", their hashes are only equal with very low probability. A collision is when two different objects have the same hash. We want to minimize the probability of this happening. As a result, hashes like the length of the string are not very good hashes. 3. A good hash should be reasonably fast to compute. One main purpose of hashing is to make equality checks between objects fast. A hash function that is hard to compute defeats the purpose of this. Every Java Object supports the hashCode() function. By default, hashCode() stores information about the memory address of the Object. When we implement a new class, we can override this function. For example, let us define the following polynomial hash for strings: 1 2 3 4 5 6 7 8 public int hashCode () { int hash = 0; for ( int k = 0; k < length () ; k ++) { hash *= 31; hash += ( int ) ( charAt ( k ) ) ; } return hash ; } In Java, a.equals(b) should imply a.hashCode() == b.hashCode(). This function produces the same result as the actual hashCode() function in the String class. However, this is not quite what we want for our hash set implementation, because in the end we wish to be able to store the objects in some kind of array. Since hashCode() can be any int, this hash not only returns integers 29 Hsiang, Wei, Liu Chapter 3. Standard Library Data Structures that can be very large, they can also be negative, and thus are not suitable as array indices. The natural way to fix this is to take the hash modulo the size of the array we want. 1 2 3 4 5 6 7 String [] table = new String [10007]; int index ( E o ) { int i = o . hashCode () % table . length ; if ( i >= 0) return i ; return i + table . length ; } The length of our array is somewhat arbitrary. We chose the number 100007 because it is a prime number, and primes are generally nice since integers modulo a prime form a field. Remember that a negative number % another number is not necessarily positive, so we need to be a little careful. From here, adding an element to the table and checking if an element is contained both seem straightforward: 1 2 3 4 5 6 7 8 boolean add ( E o ) { table [ index ( o ) ] = o ; return true ; } boolean contains ( Object o ) { int i = index (( E ) o ) ; return table [ i ] != null && table [ i ]. equals ( o ) ; } null is always annoying to deal with, and will always have to be handled separately. However, a problem quickly arises in the (hopefully unlikely) instance of a collision. If two strings have the same hash, we can’t add both to the table since there isn’t enough space in the array. The easiest way to handle a collision is by chaining. We change the hash table to store a linked list instead of a single element in the event of a collision. The hope is that not too many objects map to a single index in the array, as searching a linked list for a particular element is O(n). Java once implemented this method of resolving collisions, but recently changed it to a BST in Java 8. Here’s an example of chaining on a small array of size 5 with the characters for “cow”. The numbers below the letters represent their hashes. c and w collide. 0 1 o 2 111 3 4 c w 99 119 If we use a good hash function and a reasonable array size, collisions will almost always be 30 Hsiang, Wei, Liu Chapter 3. Standard Library Data Structures pretty evenly spread across the array. Then, since we store everything using an array, the hash table provides probabilistic O(1) time complexities for insertion, deletion, and search. The Java set implementation of a hash table is the HashSet. The C++1114 set implementation of a hash table is the unordered_set. Function Java, HashSet C++11, unordered_set insert boolean add(E e) void insert value_type& val) search boolean o) size_type count (const value_type& val) const15 delete boolean remove(Object o) size_type erase (const value_type& val), void erase (iterator position) size int size() size_type size() const 3.7 contains(Object (const Map A map is simply a function that takes a key to a value. As a map is a function, its domain, or the keys of the map, form a set, though the values need not be unique. The most generic function looks something like in the following diagram conceptually. "a" 5 "c" 3 "d" 6 "f" 2 "g" values keys In implementation, a map is very similar to a set. Since the map represents a function from the set of keys to the set of values, we want to support quick lookup and updates of the keys so we can evaluate and change the function represented by our map. The best ways to store a set for quick access and update are, as discussed in the previous section, the binary search tree and the hash table, so all we need to store is the set of keys itself with extra information for the value associated attached. In a binary search tree map, the elements are sorted by the keys. 14 15 added recently; not included in C++ 1 if in the set, 0 otherwise. 31 Hsiang, Wei, Liu Chapter 3. Standard Library Data Structures "g" 2 "d" 3 "a" 5 "f" 2 "c" 6 Similarly, for the hash table map, we use the hashes of the keys. 0 100 "d" 1 3 102 "f" 97 "a" 2 2 5 3 4 99 "c" 103 "g" 2 6 Map is a Java interface. The TreeMap is the Map variant of the TreeSet; similarly, the HashMap is the Map variant of the HashSet. map is the C++ implementation of a balanced binary search tree map, while unordered_map is the C++11 implementation of a hash table. As a map involves two kinds of data, keys and values, generics for maps therefore have two arguments, one for the key and one for the value. The following Java Map from Strings to Integers demonstrates generics with multiple arguments. 1 2 Map < String , Integer > number = new TreeMap < String , Integer >() ; email . put ( " Samuel Hsiang " , 5) ; 32 Hsiang, Wei, Liu Chapter 3. Standard Library Data Structures Function Java, TreeMap C++, map insert and update value V put(K key, V value) mapped_type& operator[] (const key_type& k)16 access V get(Object key) mapped_type& operator[] (const key_type& k) delete V remove(Object key) size_type erase (const key_type& val), void erase (iterator position) size int size() size_type size() const set of keys Set keySet() no implementation other useful search functions just look in the API >.< Function Java, HashMap C++11, unordered_map insert and update value V put(K key, V value) mapped_type& operator[] (const key_type& k) access V get(Object key) mapped_type& operator[] (const key_type& k) delete V remove(Object key) size_type erase (const key_type& val), void erase (iterator position) size int size() size_type size() const set of keys Set keySet() no implementation 16 Works like an array or vector. 33 Chapter 4 Graph Algorithms In this chapter we explore some of the most famous results in graph theory. 4.1 Connected Components A connected component of an undirected graph is a subgraph such that, for any two vertices in the component, there exists a path from one to the other. The diagram illustrates three connected components of a graph, where each vertex is colored together with its associated component. 2 6 1 7 4 9 3 5 10 8 A strongly connected component of a directed graph is a subgraph such that every vertex in the component can be reached from any other vertex in the component. 1 2 3 4 5 6 7 8 Finding the connected components of an undirected graph is a straightforward problem, while finding the strongly connected components of a directed graph is more complicated and won’t be covered in this chapter. 35 Hsiang, Wei, Liu 4.1.1 Chapter 4. Graph Algorithms Flood Fill Really any kind of search method solves the undirected graph connected components problem. We could use recursion with a depth-first search. To avoid using the run-time stack, we could use a queue to perform a breadth-first search. Both of these run in O(E + V ) time. I would recommend in general to use the BFS. 4.1.2 Union-Find (Disjoint Set Union) The union-find data structure is another way for us to solve the connected components problem. Union-find is unique from the other search techniques in that it can process input as it is presented, edge by edge. This also means it is possible to add more edges at the end, therefore changing the graph, while still running quickly. An algorithm that works like this is an online algorithm, while an algorithm that requires all the input data presented at the beginning is an offline algorithm. A natural idea for solving the connected components problem is for each vertex to maintain a pointer to another vertex it’s connected to, forming a forest, or collection of trees. To check whether two elements are in the same component, simply trace the tree up to the root by jumping up each pointer. 1 2 3 4 5 9 6 10 7 8 The idea of a pointer can easily be stored within an array. -1 1 1 2 -1 5 6 6 -1 9 1 2 3 4 5 6 7 8 9 10 We want to support two operations: f ind(v), which returns the root of the tree containing v, and union(u, v), which merges the components containing u and v. This second operation is easy given the first; simply set the pointer of f ind(u) to be f ind(v). union(4, 6), unoptimized: 36 Hsiang, Wei, Liu Chapter 4. Graph Algorithms 5 9 1 6 2 3 7 10 8 4 A problem quickly arises – the f ind operation threatens to become linear. There are two simple things we can do to optimize this. The first is to always add the shorter tree to the taller tree, as we want to minimize the maximum height. An easy heuristic for the height of the tree is simply the number of elements in that tree. We can keep track of the size of the tree with a second array. This heuristic is obviously not perfect, as a larger tree can be shorter than a smaller tree, but it turns out with our second optimization that this problem doesn’t matter. The second fix is to simply assign the pointer associated with v to be f ind(v) at the end of the f ind operation. We can design f ind(v) to recursively call f ind on the pointer associated with v, so this fix sets pointers associated with nodes along the entire chain from v to f ind(v) to be f ind(v). These two optimizations combined make the union and f ind operations O(α(V )), where α(n) is the inverse Ackermann function, and for all practical values of n, α(n) < 5. f ind(4), optimized: 5 1 3 9 6 7 2 8 37 4 10 Hsiang, Wei, Liu Chapter 4. Graph Algorithms Algorithm 1 Union-Find function Find(v) if v is the root then return v parent(v) ← Find(parent(v)) return parent(v) function Union(u, v) uRoot ← Find(u) vRoot ← Find(v) if uRoot = vRoot then return if size(uRoot) < size(vRoot) then parent(uRoot) ← vRoot size(vRoot) ← size(uRoot) + size(vRoot) else parent(vRoot) ← uRoot size(uRoot) ← size(uRoot) + size(vRoot) 4.2 Shortest Path A classic. Assign nonnegative weights to each of the edges, where the weight of the edge (u, v) represents the distance from u to v. This graph can be either directed or undirected. 4.2.1 Dijkstra Dijkstra’s algorithm solves the single-source shortest path problem. From any vertex, we can compute the shortest path to each of the remaining vertices in the graph. The two formulations of Dijkstra’s algorithm run in O(V 2 ) or O(E log V ) time, whichever one suits us better. Note that it is possible to do better than O(E log V ) using a Fibonacci heap. The former works nicely on dense graphs, as E ≈ V 2 , while the latter works better on sparse graphs, as E ≈ V . For every vertex v in the graph, we keep track of the shortest known distance dist(v) from the source to v, a boolean visited(v) to keep track of which nodes we “visited,” and a pointer to the previous node in the shortest known path prev(v) so that we can trace the shortest path once the algorithm finishes. Dijkstra iteratively “visits” the next nearest vertex, updating the distances to that vertex’s neighbors if necessary. Therefore, at any step, we have the first however-many nearest vertices to the source, which we call “visited” and for which the shortest path is known. We also have the shortest path to all the remaining vertices that stays within the “visited” vertices besides for the very last edge, if such a path exists. We claim that the known distance to the closest vertex that has not yet been visited is the shortest distance. We can then “visit” that vertex. It shouldn’t be hard to prove that this algorithm indeed calculates the shortest path. The O(V 2 ) implementation immediately follows. 38 Hsiang, Wei, Liu Chapter 4. Graph Algorithms Algorithm 2 Dijkstra for all vertices v do dist(v) ← ∞ visited(v) ← 0 prev(v) ← −1 dist(src) ← 0 while ∃v s.t. visited(v) = 0 do v ≡ v s.t. visited(v) = 0 with min dist(v) visited(v) ← 1 for all neighbors u of v do if visited(u) = 0 then alt ← dist(v) + weight(v, u) if alt < dist(u) then dist(u) ← alt prev(u) ← v 4 6 4 2 3 2 4 5 5 3 7 1 Let’s run Dijkstra’s algorithm on the above graph with vertex 1 as the source. We first set all the distances besides the source to be ∞. 4 ∞ 6 2 ∞ 4 4 5 3 ∞ 1 0 5 ∞ 3 2 7 39 Hsiang, Wei, Liu Chapter 4. Graph Algorithms Now, we continue choosing the closest unvisited node, mark it as visited, and and update its neighbors. 4 ∞ 6 2 4 4 5 ∞ 3 2 4 5 3 7 1 0 7 4 10 6 2 4 4 5 ∞ 3 2 4 5 3 6 1 0 7 4 9 6 4 2 4 4 5 3 6 1 0 5 14 3 2 7 40 Hsiang, Wei, Liu Chapter 4. Graph Algorithms 4 9 6 4 2 4 5 14 3 2 4 5 3 6 1 0 7 4 9 6 4 2 4 5 14 3 2 4 5 3 6 1 0 7 The slow part of the O(V 2 ) formulation is the linear search for the vertex v with the minimum dist(v). We happen to have a data structure that resolves this problem – a binary heap. The main problem with using the standard library heap is having repeated vertices in the heap. We could just ignore this problem and discard visited vertices as they come out of the heap. Alternatively, we could choose never to have repeated vertices in the heap. To do this, we need to be able to change the value of the distances once they are already in the heap, or decrease-key. This is a pretty simple function to add, however, if you have a heap already coded. Either way, we achieve O(E log V ), as we do E + V updates to our heap, each costing O(V ). 4.2.2 Floyd-Warshall Dijkstra is nice when we are dealing with edges with nonnegative weights and are looking for the distances from one vertex to all the others. Floyd-Warshall solves the shortest path problem for all pairs of vertices in O(V 3 ) time, which is faster than V single-source Dijkstra runs on a dense graph. Floyd-Warshall works even if some edge weights are negative but not if the graph has a negative cycle. 41 Hsiang, Wei, Liu Chapter 4. Graph Algorithms Algorithm 3 Floyd-Warshall for all vertices v do dist(v, v) = 0 for all edges (u, v) do dist(u, v) = weight(u, v) for all vertices k do for all vertices i do for all vertices j do if dist(i, j) > dist(i, k) + dist(k, j) then dist(i, j) ← dist(i, k) + dist(k, j) 4.2.3 Bellman-Ford Bellman-Ford is a single-source O(V E) shortest path algorithm that works when edge weights can be negative. It is preferable to Floyd-Warshall when the graph is sparse and we only need the answer for one source. Like Floyd-Warshall, the algorithm fails if the graph contains a negative cycle, but the algorithm is still useful for detecting negative cycles. The idea here is the shortest path, assuming no negative cycles, has length at most V − 1. Algorithm 4 Bellman-Ford for all vertices v do dist(v) ← ∞ prev(v) =← −1 dist(src) ← 0 for i ≡ 1, V − 1 do for all edges (u, v) do if dist(u) + weight(u, v) < dist(v) then dist(v) ← dist(u) + weight(u, v) prev(v) ← u for all edges (u, v) do if dist(u) + weight(u, v) < dist(v) then negative cycle detected 4.3 . check for negative cycles Minimum Spanning Tree Consider a connected, undirected graph. A spanning tree is a subgraph that is a tree and contains every vertex in the original graph. A minimum spanning tree is a spanning tree such that the sum of the edge weights of the tree is minimized. Finding the minimum spanning tree uses many of the same ideas discussed earlier. 42 Hsiang, Wei, Liu Chapter 4. Graph Algorithms 4 6 4 2 3 2 4 5 5 3 1 4.3.1 7 Prim Prim’s algorithm for finding the minimum spanning tree is very similar to Dijkstra’s algorithm for finding the shortest path. Like Dijkstra, it iteratively adds a new vertex at a time to build a tree. The only difference is dist(v) stores the shortest distance from any visited node instead of the source. Algorithm 5 Prim for all vertices v do dist(v) ← ∞ visited(v) ← 0 prev(v) ← −1 dist(src) ← 0 while ∃v s.t. visited(v) = 0 do v ≡ v s.t. visited(v) = 0 with min dist(v) visited(v) ← 1 for all neighbors u of v do if visited(u) = 0 then if weight(v, u) < dist(u) then dist(u) ← weight(v, u) prev(u) ← v The proof of correctness is left as an exercise. The complexity of this algorithm depends on how the minimum unvisited vertex is calculated. Using the same approaches as Dijkstra, we can achieve O(V 2 ) or O(E log V ). 4.3.2 Kruskal While Prim greedily adds vertices to the tree, Kruskal’s algorithm greedily adds edges. It iterates over all the edges, sorted by weight. We need to watch out for adding a cycle, breaking the tree structure, which means we need to keep track of each vertex’s connected component. If an edge 43 Hsiang, Wei, Liu Chapter 4. Graph Algorithms connects two vertices from the same connected component, we don’t want to add it to our tree. However, we have a union-find algorithm that works perfectly for this. Algorithm 6 Kruskal for all edges (u, v) in sorted order do if Find(u) 6= Find(v) then add (u, v) to spanning tree Union(u, v) This algorithm requires a sort of the edges and thus has complexity O(E log E) = O(E log V ). 4.4 Eulerian Tour An Eulerian tour of a graph is a path that traverses every edge exactly once. If the tour ends exactly where it started, it is called an Eulerian circuit. A graph has an Eulerian circuit if it is connected and every vertex has even degree. A graph has an Eulerian path if it is connected and all vertices but exactly two have even degrees. The mathematical proofs for these graph properties hinge on the idea that removing a cycle from the graph maintains the Eulerian property. We construct an Eulerian tour by appealing to this idea. Algorithm 7 Eulerian Tour function FindTour(v) while v has a neighbor u do delete edge (v, u) FindTour(u) add v to tour . FindTour(u) must trace a circuit back to v It is not preferable to use the run-time stack; we can use our own stack if necessary. If the graph contains an Eulerian circuit, we call this function on any vertex we like. If it contains an Eulerian path, we call this function on one of the vertices with odd degree. 44 Chapter 5 Complex Ideas and Data Structures Here we build on previous material to introduce more complex ideas that are useful for solving USACO Gold problems and beyond. 5.1 Dynamic Programming over Subsets (n2n DP) We’ve already covered how dynamic programming can turn exponential solutions into polynomial solutions, but it can also help turn factorial solutions into exponential. Problems where the bound on n is 20, for example, signal that an exponential solution is the one required. Consider the following problem: (USACO December 2014, guard) Farmer John and his herd are playing frisbee. Bessie throws the frisbee down the field, but it’s going straight to Mark the field hand on the other team! Mark has height H (1 ≤ H ≤ 1, 000, 000, 000), but there are N cows on Bessie’s team gathered around Mark (2 ≤ N ≤ 20). They can only catch the frisbee if they can stack up to be at least as high as Mark. Each of the N cows has a height, weight, and strength. A cow’s strength indicates the maximum amount of total weight of the cows that can be stacked above her. Given these constraints, Bessie wants to know if it is possible for her team to build a tall enough stack to catch the frisbee, and if so, what is the maximum safety factor of such a stack. The safety factor of a stack is the amount of weight that can be added to the top of the stack without exceeding any cow’s strength. We can try the O(N !) brute force, trying every permutation of cows possible. However, this is far too slow. N ≤ 20 hints at an exponential solution, so we think of trying every possible subset of the cows. Given a subset S of cows, the height reached is the same, so perhaps we sort the subset by strength, and put the strongest cow on the bottom. We see that this greedy approach fails: suppose that the first cow has weight 1 and strength 3 and the second cow has weight 4 and strength 2. Greedy would tell us to put the first cow on the bottom, but this fails, while putting the second cow on the bottom succeeds. When greedy fails, the next strategy we look at is dynamic programming. To decide whether S is stable, we have to find whether there exists a cow j in S that can support the weight of all the other cows in S. But how do we know whether the set S \ {j} is stable? This is where dynamic programming comes in. 45 Hsiang, Wei, Liu Chapter 5. Complex Ideas and Data Structures This leads to a O(N 2N ) solution. This seems like a pain to code iteratively, but there is a nice fact about subsets: there is a cute bijection from the subsets of {0, 1, 2, . . . , N − 1} to the integers from 0 to 2N − 1. That is, the subset {0, 2, 5, 7} maps to 20 + 22 + 25 + 27 = 165 in the bijection. We call this technique masking. We require all the subsets of S to be processed before S is processed, but that property is also handled by our bijection, since subtracting a power of 2 from a number decreases it. With a little knowledge of bit operators, this can be handled easily. for i ← 0, 2N − 1 do . i represents the subset S dp(i) ← −1 for all j ∈ S do . j ∈ S satisfy i & (1 « j) != 0 P alt ← min(dp(i − 2j ), strength(j) − k∈S\{j} weight(k)) if dp(i) < alt then dp(i) ← alt & is the bitwise and function, while « is the left shift operator. √ 5.2 n Bucketing √ n bucketing is a relatively straightforward idea – given n elements {xi }ni=1 in a sequence, we group √ them into n equal-sized buckets. The motivation for arranging elements like this is to support an operation called a range query. Let’s take a concrete example. Suppose we want to support two operations: • update(p, k) – increment the value of xp by k • query(a, b) – return Pb i=a xi . Suppose we simply stored the sequence in an array. update then becomes an O(1) operation, but query is O(n). 2 4 7 −5 3 6 −3 1 −2 −4 −6 2 8 6 0 −7 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Another natural approach would be to store in a separate array the sum of the first i terms in the sequence for every index i, or store the prefix sums. 0 2 6 13 8 11 17 14 15 13 9 3 5 13 19 19 12 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Now query becomes an O(1) operation, as we can simply subtract two elements in the array to answer a query. Unfortunately, update becomes O(n), as changing the value of an element in the beginning of the sequence forces us to change almost all the values in the prefix sum array. We can still use this idea, though... what we are looking for is some way to group values into sums such that we only need to change a small number of the sums to update and only require a small number of them to query. √ This leads us directly to a n bucketing solution. Let’s group the 16 elements into 4 groups. 46 Hsiang, Wei, Liu Chapter 5. Complex Ideas and Data Structures 2 4 7 −5 3 6 −3 1 −2 −4 −6 2 8 6 0 −7 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 8 7 −10 7 [1, 4] [5, 8] [9, 12] [13, 16] We’ll keep track of the total sum of each group. Now, if we want to update a value, we need to change only two values – the value of that element in the original array and the total sum of the bucket it is in. When we query a range, we’ll take advantage of the sum of the bucket when we can. Highlighted are the numbers we’ll need for query(7, 15). 2 4 7 −5 3 6 −3 1 −2 −4 −6 2 8 6 0 −7 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 8 7 −10 7 [1, 4] [5, 8] [9, 12] [13, 16] √ √ Querying requires access to at most n bucket sums and 2( n − 1) individual values. Therefore √ √ we have O( n) query and O(1) update. We are able to improve O( n) update to O(1) because of nice properties of the + operator. This is not always the case for range queries: suppose, for instance, we needed to find the minimum element on a range. √ It is often the case that O( n) bounds can be improved to O(log n) using more complex data structures like segment trees and more complex ideas like 2n jump pointers, both of which are covered in this chapter. These are, however, more complicated to implement and as such are often √ comparable in runtime in the contest environment. Steven Hao is notorious for using crude n bucketing algorithms to solve problems that should have required tighter algorithm complexities. √ n bucketing is a crude yet powerful idea; always keep it in the back of your mind. 5.3 Segment Tree For our range sum query problem, it turns out that we can do as well as O(log n) with a segment tree, also known as a range tree or augmented static BBST. The essential idea is still the same—we want to group elements in some way that allows us to update and query efficiently. As the name “tree” suggests, we draw inspiration from a binary structure. Let’s build a tree on top of the array, where each node keeps track of the sum of the numbers associated with its children. Every node keeps track of the range it is associated with and the value of the sum of all elements on that range. For example, the root of the tree is hresponsible j ki for the sum of all elements in the 1+n range [1, n], its left child is responsible for the range 1, 2 and its right child is responsible for hj 1+n 2 k i + 1, n . 47 Hsiang, Wei, Liu Chapter 5. Complex Ideas and Data Structures h j In general, for the vertex responsible for the range [l, r], its left child holds the sum for l, hj k l+r 2 ki i and its right child l+r + 1, r . As we go down to the tree, eventually we’ll have nodes with ranges 2 [l, l] that represent a single element in the original list. These, of course, will not have any children. 12 −3 15 8 −10 7 6 2 −2 9 −6 7 −4 −7 14 2 4 7 −5 3 6 −3 1 −2 −4 −6 2 8 6 0 −7 2 4 7 −5 3 6 −3 1 −2 −4 −6 2 8 6 0 −7 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Highlighted are the nodes we’ll need to access for query(7, 15). Notice how the subtrees associated with each of these nodes neatly covers the entire range [7, 15]. 12 −3 15 8 6 2 2 4 −10 7 7 −2 9 −5 3 6 −3 −6 −2 1 −4 7 −4 −6 −7 14 2 8 6 0 −7 −2 represents the sum x7 + x8 , −10 the sum x9 + x10 + x11 + x12 , 14 the sum x13 + x14 , and 0 represents the single element x15 . It seems we always want to take the largest segments that stay within the range [7, 15]. But how do we know exactly which segments these are? 48 Hsiang, Wei, Liu Chapter 5. Complex Ideas and Data Structures We handle queries using a recursive call, starting at the root of the tree. We then proceed as follows: If the current node’s interval is completely disjoint from the queried interval, we return 0. If the current node’s interval is completely contained within the queried interval, we return the sum associated with that node. Otherwise, we pass the query on to the node’s two children. Note that this process is O(log n) because each level in the tree can have at most two highlighted nodes. function Query(range [l, r], range [a, b]) . at node [l, r], want bi=a xi if r < a or b < l then . [l, r] ∩ [a, b] = ∅ return 0 if a ≤ l and r ≤ b then . [l, r] ⊆ [a, b] return sum(l, r) h j ki hj k i l+r return Query( l, l+r , [a, b]) + Query( + 1, r , [a, b]) 2 2 P Segment trees also handle modifications well. If we want to change the third element to 2, then we have to update the highlighted nodes in the following diagram. We can implement this the same way we implement queries. Starting from the root, we update each modified node’s children before recomputing the value stored at that node. The complexity is O(log n); we change the value of one node in each level of the tree. 7 −3 10 3 −3 6 2 4 −10 7 −5 2 −2 9 3 6 −3 −6 −2 1 hj l+r 2 k −4 −6 −7 14 2 8 6 0 −7 . x p ← xp + k . p 6∈ [l, r] function Update(range [l, r], p, k) if r < p or p < l then return if l = r then sum(l, r) ← k return h j ki , p, k) Update( l, l+r 2 Update( −4 7 . leaf node i + 1, r , p, k) sum(l, r) ← sum(l, j l+r 2 k ) + sum( j l+r 2 k + 1, r) I cheated with my example by using a nice power of two, n = 16, as the number of elements in the sequence. Of course, the size is not always this nice. However, if we want a tree of size n, we can always round up to the nearest power of 2, which is at most 2n. A perfectly balanced segment 49 Hsiang, Wei, Liu Chapter 5. Complex Ideas and Data Structures tree of size n equaling a power of 2 requires 2n − 1 nodes representing our segments, so 4n bounds the memory we’ll need. Now all we need is a way for us to easily store the sum values associated with each node. We can clearly use a structure with two child pointers, but we can also exploit the fact that the segment tree structure is a balanced binary search tree. We can store the tree like we store a heap. The root is labeled 1, and for each node with label i, the left child is labeled 2i and the right child 2i + 1. Here’s some sample code. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 final int SZ = 1 << 17; // some sufficiently large power of 2 int [] sum = new int [2 * SZ ]; // sum [1] contains sum for [1 , SZ ] , and so on int query ( int i , int l , int r , int a , int b ) { // i . e . query (1 , 1 , SZ , 7 , 15) if ( r < a || b < l ) return 0; if ( a <= l && r <= b ) return tree [ i ]; int m = ( l + r ) / 2; int ql = query (2 * i , l , m , a , b ) ; int qr = query (2 * i + 1 , m + 1 , r , a , b ) ; return ql + qr ; } void update ( int i , int l , int r , int p , int k ) { // i . e . update (1 , 1 , SZ , 3 , 2) if ( r < p || p < l ) return ; if ( l == r ) { sum [ i ] = k ; return ; } int m = ( l + r ) / 2; update (2 * i , l , m , p , k ) ; update (2 * i + 1 , m + 1 , r , p , k ) ; sum [ i ] = sum [2 * i ] + sum [2 * i + 1]; } Mathematically, what allows the segment tree to work on the addition operation lies in the fact that addition is an associative operation. This means that we can support a wide variety of other kinds of range queries, so long as the operation is associative. For example, we can also support range minimum queries and gcd and lcm queries. We can even combine different types of information stored at a node. One situation that requires this is maintaining the maximum prefix sum. For our simple range sum query problem, we don’t need the nice, completely balanced structure present when the number of elements is a nice power of two. However, it is necessary if we want to force the array sum[] to have the same nice properties as an actual heap so we can perform nice iterative operations on our tree, as previously, all tree operations were recursive. It is also necessary if we need the numbers representing the indices in our tree to have special properties, as in the Fenwick tree. 5.3.1 Lazy Propagation It is often the case that in addition to performing range queries, we need to perform range updates. (Before, we only had to implement point updates.) One extension of our sum problem would require the following two functions: 50 Hsiang, Wei, Liu Chapter 5. Complex Ideas and Data Structures • update(a, b, k) – increment the value of xi by k for all i ∈ [a, b] • query(a, b) – return Some Motivation: √ Pb i=a xi . n Blocking √ Let’s go back to our n blocking solution and see what changes we can make, and hopefully we √ can extend this idea back to our segment tree. If we’re looking for an O( n) implementation for update, we clearly can’t perform point updates for all values in the range. The way we sped up query was by keeping track of an extra set of data, the sum of all the elements in a bucket, which we used when the entire bucket was in the query range. 2 4 7 −5 3 6 −3 1 −2 −4 −6 2 8 6 0 −7 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 8 7 −10 7 [1, 4] [5, 8] [9, 12] [13, 16] Can we do something similar for update? The case we need to worry about is when an entire bucket is included in the update range. Again, we don’t want to touch the original array a at all, since that makes the operation linear. Instead, whenever we update an entire bucket, we track the information about the update separately. Thus we store a value for each bucket indicating the amount by which we’ve incremented that entire bucket. With this in mind, highlighted are the elements we’ll need for update(4, 14, 3). 2 4 7 −2 3 6 −3 1 −2 −4 −6 2 11 9 0 −7 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 11 7 −10 13 [1, 4] [5, 8] [9, 12] [13, 16] 0 3 3 0 [1, 4] [5, 8] [9, 12] [13, 16] To reiterate, we are not storing the actual values of the elements or buckets in the arrays, where they were stored when we solved the original formulation of the problem. Despite l this m fact, we can i √ still calculate the value of any element or bucket. ai is equal to the sum of the th value stored n in the third array and the ith value stored in the first array. The sum of any given bucket can be calculated similarly. However, we must remember to adjust for bucket size. In the example, there are four elements per bucket, so we have to add 4 · 3 = 12 to get the correct sum for an updated bucket. Because of all this, we can query a range exactly like we did without range updates. Highlighted are the values necessary for query(7, 15). 51 Hsiang, Wei, Liu Chapter 5. Complex Ideas and Data Structures 2 4 7 −2 3 6 −3 1 −2 −4 −6 2 11 9 0 −7 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 11 7 −10 13 [1, 4] [5, 8] [9, 12] [13, 16] 0 3 3 0 [1, 4] [5, 8] [9, 12] [13, 16] √ Thus we have achieved an O( n) solution for both range updates and and range queries. Lazy Propagation on a Segment Tree Motivated by how we fixed our bucketing solution, let’s try adding a similar extra piece of information to our segment tree to try to get an O(log n) solution. Call this extra value the “lazy” value. 12 0 −3 0 15 0 8 0 6 0 2 0 2 0 4 0 −10 0 7 0 7 0 −2 0 9 0 −5 0 3 0 6 0 −3 0 −6 0 −2 0 1 0 7 0 −4 0 −4 0 −6 0 −7 0 14 0 2 0 8 0 6 0 0 0 −7 0 Once again, if the entire range associated with a node is contained within the update interval, we’ll just make a note of it on that particular node and not update that node or any of its children. We’ll call such a node “lazy.” Here’s the status of the tree after update(3, 12, 2). 52 Hsiang, Wei, Liu Chapter 5. Complex Ideas and Data Structures 32 0 27 0 5 0 12 0 6 0 2 0 2 2 4 0 −10 2 7 2 7 0 −2 0 9 0 −5 0 3 0 6 0 −3 0 −6 0 −2 0 1 0 7 0 −4 0 −4 0 −6 0 −7 0 14 0 2 0 8 0 6 0 0 0 −7 0 When a node is lazy, it indicates that the sum numbers of every node in its subtree are no longer accurate. In particular, if a node is lazy, the sum number it holds is not equal to the sum of the values in its leaves. This means that whenever we need to access any node in the subtree of that node, we’ll need to update some values. Suppose we encounter a lazy node while traversing down the tree. In order to get accurate sum values in that node’s subtree, we need to apply the changes indicated by its lazy value. Thus we update the node’s sum value, incrementing by the lazy value once for each leaf in the node’s subtree. In addition, we have to propagate the lazy value to the node’s children. We do this by incrementing each child’s lazy value by our current node’s lazy value. And finally, we need to set the lazy value to 0, to indicate that there’s nothing left to update. When we implement a segment tree, all of this is usually encapsulated within a “push” function. Let’s illustrate querying with an example: query(7, 13). To answer this, we need to access the nodes for the ranges [7, 8], [9, 12], and [13, 13]. The node for [13, 13] is up-to-date and stores the correct sum. However, the other two nodes do not. (The node for [7, 8] is in the subtree of the node for [5, 8], which is lazy.) Thus as we recurse, we push our lazy values whenever we encounter them. Highlighted are the nodes we’ll need to update for the query. Notice how [5, 8] and [9, 12] simply pass their lazy numbers to their children, where they’ll update themselves when necessary. 53 Hsiang, Wei, Liu Chapter 5. Complex Ideas and Data Structures 32 0 27 0 5 0 12 0 6 0 2 0 2 2 4 0 −2 0 15 0 7 0 9 2 −5 0 3 0 −6 2 2 0 6 0 −3 2 7 0 −2 0 1 2 −4 2 −4 0 −6 0 −7 0 14 0 2 0 8 0 6 0 0 0 −7 0 The complexity of querying remains the same, even when propagating lazy values. We have to push at most once for each node we encounter, so our runtime is multiplied only by a constant factor. Thus, like normal segment tree queries, lazy segment tree queries also run in O(log n). While we’re thinking about complexity, we can also ask ourselves why it doesn’t take O(n) time to update O(n) nodes. With lazy propagation, we take advantage of two things. The first is that on each query, we access very few nodes, so as long as the nodes we access are up-to-date, we’re all set. The second is that we can combine updates while they’re still being propagated, that the update operation is associative. This allows us to update only when it’s absolutely necessary—the rest of the time, we can be lazy. (The next time someone tells you you’re being lazy, you can say it’s a good thing.) Like normal segment trees, lazily propagating segment trees can handle a diverse set of range updates and range queries. We can support an update, where instead of incrementing each element on a range by a certain value, we set each element to that value. There is no difference between the two as point updates, but they are very different operations when applied as range updates. Sometimes, we can even have more than one range update on a single tree. When implementing this, however, it is important to be careful when pushing lazy values—composing different operations can become quite complicated. Below is my implementation of a segment tree supporting range sum and range increment. Note that it includes one detail that we left out in our development of lazy propagation above: how we update a node whose range partially intersects the updated range. One way to do this is to calculate the length of the intersection and update directly. However, this does not work well for queries that are not as nice as incrementing, such as setting an entire range to a value. Instead, we first update the children of this node. Then we push the lazy values off the children so their sum values are accurate. This allows us to recalculate the sum value of the parent like we do for a non-lazy segtree. I have this as my pull function below. 54 Hsiang, Wei, Liu 1 2 3 Chapter 5. Complex Ideas and Data Structures final int SZ = 1 << 17; int [] sum = new int [2 * SZ ]; int [] lazy = new int [2 * SZ ]; 4 5 6 7 8 9 10 11 12 13 14 void push ( int i , int l , int r ) { if ( lazy [ i ] != 0) { sum [ i ] += ( r - l + 1) * lazy [ i ]; if ( l < r ) { lazy [2 * i ] += lazy [ i ]; lazy [2 * i + 1] += lazy [ i ]; } lazy [ i ] = 0; } } 15 16 17 18 19 20 21 void pull ( int i , int l , int r ) { int m = ( l + r ) / 2; push (2 * i , l , m ) ; push (2 * i + 1 , m + 1 , r ) ; sum [ i ] = sum [2 * i ] + sum [2 * i + 1]; } 22 23 24 25 26 27 28 29 30 31 32 33 int query ( int i , int l , int r , int a , int b ) { push (i , l , r ) ; if ( r < a || b < l ) return 0; if ( a <= l && r <= b ) { return sum [ i ]; } int m = ( l + r ) / 2; int ql = query (2 * i , l , m , a , b ) ; int qr = query (2 * i + 1 , m + 1 , r , a , b ) ; return ql + qr ; } 34 35 36 37 38 39 40 41 42 43 44 45 46 void update ( int i , int l , int r , int a , int b , int k ) { // push (i , l , r ) ; // Necessary for non - commutative range updates . if ( r < a || b < l ) return ; if ( a <= l && r <= b ) { lazy [ i ] += k ; return ; } int m = ( l + r ) / 2; update (2 * i , l , m , a , b , k ) ; update (2 * i + 1 , m + 1 , r , a , b , k ) ; pull (i , l , r ) ; } 5.3.2 Fenwick Tree A Fenwick tree, or binary indexed tree (BIT), is simply a faster and easier to code segment tree when the operator in question, in addition to being associative, has an inverse. Unfortunately, it’s not at all intuitive, so bear with me at first and let the magic of the Fenwick tree reveal itself later.1 1 In fact, it is so magical that Richard Peng hates it because it is too gimmicky. 55 Hsiang, Wei, Liu Chapter 5. Complex Ideas and Data Structures The key idea is to compress the data stored within a segment tree in a crazy way that ends up having a really slick implementation using some bit operation tricks. As discussed earlier, the + operator has an inverse, −. Therefore, there is an inherent redundancy, for example, in keeping track of the sum of the first n2 elements, the sum of all n elements, and the sum of the last n2 elements, as we do in the segment tree. If we are given only Pn Pn k=1 ak , we can find k=n/2+1 ak easily using subtraction. Pn/2 k=1 ak and With this in mind, let’s ignore every right child in the tree. We’ll mark them as black in the diagram. After that, we’ll write out the tree nodes in postfix traversal order, without writing anything whenever we encounter a black node. 12 −3 15 8 −10 7 6 2 −2 9 −6 7 −4 −7 14 2 4 7 −5 3 6 −3 1 −2 −4 −6 2 8 6 0 −7 2 4 7 −5 3 6 −3 1 −2 −4 −6 2 8 6 0 −7 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 2 6 7 8 3 9 −3 15 −2 −6 −6 −10 8 14 0 12 1 2 3 4 5 6 7 8 9 10 11 13 14 15 16 12 Our Fenwick tree is simply this last array. This should be quite confusing – it is not at all clear why this array resembles a tree, and the numbers in the array make no sense whatsoever right now. Notice that the final position of every unblackened node is just the rightmost black child in its subtree. This leads to the fact that the ith element in the Fenwick tree array is the sum yi = i X xk , k=i−2v2 (i) +1 where 2v2 (i) is simply the greatest power of 2 that divides i. Let’s look at a new diagram that hopefully will better illustrate this key property of the random array we just came up with. 56 Hsiang, Wei, Liu Chapter 5. Complex Ideas and Data Structures 12 15 −10 8 6 7 2 −6 9 −3 3 −2 14 −6 8 0 2 4 7 −5 3 6 −3 1 −2 −4 −6 2 8 6 0 −7 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 2 6 7 8 3 9 −3 15 −2 −6 −6 −10 8 14 0 12 1 2 3 4 5 6 7 8 9 10 11 13 14 15 16 12 All the framework is now in place. Now we need to find out how to query and update the Fenwick tree. Suppose we wanted to find the sum elements we need. P11 k=1 xk . Let’s take a look at the diagram to see which 12 15 −10 8 6 2 −6 9 7 −3 3 −2 14 −6 8 0 2 4 7 −5 3 6 −3 1 −2 −4 −6 2 8 6 0 −7 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 2 6 7 8 3 9 −3 15 −2 −6 −6 −10 8 14 0 12 1 2 3 4 5 6 7 8 9 10 11 13 14 15 16 12 We see that the sum 11 k=1 xk = y8 + y10 + y11 . If we look at 11 in binary, we have 11 = 010112 . Let’s see if we can find a pattern in these numbers in binary: P 57 Hsiang, Wei, Liu Chapter 5. Complex Ideas and Data Structures 11 = 010112 , 10 = 11 − 2 v2 (11) = 010102 , 8 = 10 − 2 v2 (10) = 010002 , 0 = 8 − 2v2 (8) = 000002 . So, we can simply subtract 11 − 2v2 (11) = 10 = 010102 , find the sum of the first 10 elements, and add b11 to that sum to get the sum of the first 11 elements. We see that repeating this process takes off the last 1 in the binary representation of the number i, and since there are at most log n + 1 1s in the binary representation ∀i ∈ [1, n], the query operation is O(log n). And now for the update operation. Suppose we want to change the value of x11 from −6 to −3. Which numbers will we have to change? 15 15 −7 8 6 2 −6 9 7 −3 3 −2 14 −3 8 0 2 4 7 −5 3 6 −3 1 −2 −4 −3 2 8 6 0 −7 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 2 6 7 8 3 9 −3 15 −2 −6 −3 −7 8 14 0 15 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 We needed to increment the highlighted values, y11 , y12 , and y16 , by 3. Once again we’ll look at 11, 12, and 16 in base 2. 11 = 010112 , 12 = 011002 = 11 + 2v2 (11) , 16 = 100002 = 12 + 2v2 (12) . It appears that instead of subtracting the largest dividing power of 2, we are adding. Once again this is an O(log n) operation. The real magic in the Fenwick tree is how quickly it can be coded. The only tricky part is finding exactly what 2v2 (i) is. It turns out, by the way bits are arranged in negative numbers, this is 58 Hsiang, Wei, Liu Chapter 5. Complex Ideas and Data Structures just i & -i. With this in mind, here’s all the code that’s necessary to code a Fenwick tree. Note that here, values in the array remain 1-indexed, which is different from how we code segment trees. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 int [] y = new int [ MAXN ]; // Fenwick tree stored as array void update ( int i , int x ) { for ( ; i < MAXN ; i += i & -i ) y [ i ] += x ; } int prefixSum ( int i ) { int sum = 0; for ( ; i > 0; i -= i & -i ) sum += y [ i ]; return sum ; } int query ( int i , int j ) { return prefixSum ( j ) - prefixSum ( i - 1) ; } 5.4 Queue with Minimum Query Suppose we wanted a list data structure with the following three operations: • add(x) – add x to the end of the list. • remove() – remove the first element in the list. • min() – return the minimum element in the list. This is different from a heap since remove does not remove the minimum element. It’s pretty easy to find a O(log n) solution using the data structures we already know. However, it is possible to build a data structure that can do any of these operations with complexity O(1). To solve this problem, we’ll first solve an easier problem. Suppose instead of removing the first element of the list, we had remove the last element; in other words, we needed to build a stack with minimum query instead of a queue. This is a simple task; we’ll just use a normal stack, but instead of storing single numbers, we’ll store pairs. The pairs will each contain the number we’re adding and the minimum element up to that point in the stack. To build a queue given a stack with minimum query, we’ll just have two stacks. When we add an element, we push it to the top of the first stack. When we remove an element, we take it off the top of the second stack. The minimum element in the queue is the smaller element between the minima of either stack. This seems like it obviously doesn’t work – one of the stacks keeps growing, while the other can only shrink. This is not a problem, however; when the second stack runs out of elements, we’ll just pop off every element of the first stack and push each onto the second stack. This amounts to one O(n) operation for every n O(1) operations, which averages out to constant time. 59 Hsiang, Wei, Liu 5.5 Chapter 5. Complex Ideas and Data Structures Balanced Binary Search Tree Recall that a normal binary search tree is not guaranteed to be balanced. Many data structures have been invented to self-balance the binary search tree. The red-black tree was an early development that represents the “just do it” approach, using casework to balance a tree. This results in a simple mathematical proof for the maximum depth of the tree. Unfortunately, coding these up are quite nasty do to the casework involved, so doing so is generally not preferred unless your name is Sreenath Are. The splay tree is a data structure that guarantees amortized logarithmic performance; this means that any single operation is not necessarily linear, but over any sequence of length m for m ≥ n, the performance for all m operations is O(m log n). Though the proof of the splay tree’s complexity is necessarily more complicated than the red-black tree’s complexity, as it requires amortized analysis, coding the splay tree is much easier than coding the red-black tree. In addition, the splay tree grants us more flexibility than the red-black tree provides, allowing us to build more complicated structures like link-cut trees using splay trees. The treap is a probabilistic data structure that combines a binary search tree with a heap to create a BBST. Just as quicksort is not guaranteed to run in O(n log n), treaps are not guaranteed to have logarithmic depth. In practice, however, they almost always have performance that is O(log n). Treaps are especially useful in programming contests because they are the easiest BBST to implement. 5.5.1 Treap A treap is a binary search tree smushed together with a heap. Each node is assigned, in addition to its key, a random priority. The priorities in a treap always satisfy the heap property: for any node, its priority is greater than the priority of either of its children. The diagram below illustrates a treap. On each node, the letter is the node’s key and the number is the node’s priority. E 9 D 6 H 8 B 4 A 1 F 5 C 2 I 7 G 3 The randomness in each node’s priority balances the treap. We can gain some intuition for why this works by looking at how a treap is built. Consider an array containing pairs of key and priority, sorted by key. To construct a treap from these values, we first take the element with the highest priority and set it as the root. We can then recurse on the two halves of the array split by the root, 60 Hsiang, Wei, Liu Chapter 5. Complex Ideas and Data Structures and construct the root’s left and right subtrees in a similar way. Because the priority of each node is randomly chosen, we effectively choose a random element as the root each time we recurse. Like quicksort, we can expect that the root doesn’t always split the tree too unevenly and obtain a O(log n) bound on the expected depth of each node. Moreover, it is possible to show that the entire tree has a depth of O(log n) with high probability. This analysis means that a treap is as good complexity-wise as any other deterministic BBST for all practical purposes. (To do the first part more rigorously, we can let Ai,j be an indicator random variable representing whether the jth P node is an ancestor of the ith node. What is the expected value of nj=1 Ai,j ?) In the remainder of this section, we’ll go over the implementation of treaps. Our implementation will be quite different from the rotation-based implementations of splay trees and red-black trees described above. Instead of rotating, we will have two functions, split and merge, that will be used to define all other treap operations. Here are their definitions: split takes a treap T and a key k and splits T into two treaps by k. Thus split outputs two treaps, L and R, where all the elements of L are less than k and all the elements of R are at least k. split has a complexity of O(log n). merge does the opposite of split. It takes two treaps L and R, where each key in L is at most the smallest key in R, and turns them into one treap, T . merge also has a complexity of O(log n). One way to think about these functions is to imagine treaps as a data structure that dynamically maintains sorted arrays. Then splitting is cutting an array into two, and merging is appending one array onto the end of another. With this perspective on split and merge, standard BST operations are not difficult to write. For example, we can insert a key k into a treap T by splitting T into L and R by k. Then we can create a new node n with key k, and merge L, n and R back into T . Other operations that we can implement are erase, union and intersect. Writing split and merge is also relatively easy. With C++, we can use pointers and references to simplify our work. To split, we recurse on T , while maintaining references to L and R. The root of T becomes the root of one of L and R, depending on its key. If the root of T becomes the root of R, we recurse on the left subtree of T and split into L any nodes that have key less than k. The other case, where the root of T becomes the root of L, works similarly. To merge, we take the root of L and R with the higher priority, and set it as the root of T . If the root of L has higher priority, we merge R and the right subtree of L. Otherwise, we merge L and the left subtree of R. Like split, we can recurse down the relevant subtrees until we are done. Here’s a short C++ implementation of a treap with split, merge and insert: 61 Hsiang, Wei, Liu 1 2 3 4 5 Chapter 5. Complex Ideas and Data Structures struct node { node *l , * r ; int k , p ; node ( int k ) : l (0) , r (0) , k ( k ) , p ( rand () ) {} }; 6 7 8 9 10 11 12 void split ( node *t , node *& l , node *& r , int k ) { // Splits t into l and r . if ( t == NULL ) l = r = NULL ; else if ( k <= t -> k ) split ( t -> l , l , t -> l , k ) , r = t ; else split ( t -> r , t -> r , r , k ) , l = t ; } 13 14 15 16 17 18 19 void merge ( node *& t , node *l , node // Merges l and r into t . if ( l == NULL || r == NULL ) t = l else if ( l -> p > r -> p ) merge ( l else merge ( r -> l , l , r -> l ) , t } *r){ ? l : r; -> r , l -> r , r ) , t = l ; = r; 20 21 22 23 24 25 26 void insert ( node *& t , int k ) { node * n = new node ( k ) , * a ; split (t , t , a , k ) ; merge (t , t , n ) ; merge (t , t , a ) ; } We can also create an implicitly keyed treap, which functions as a dynamic array. Instead of storing keys, each node stores the size of its subtree. We can then define the implicit key of a node as its position in the in-order traversal of the treap, a value we can easily compute from subtree sizes. Modifying split to operate based on the implicit key allows us to rearrange parts of the array and insert new elements anywhere. Further augmentation allows us to perform range queries as well. And if we add lazy propagation, we can also support operations that modify ranges. Here’s an example of an implicitly keyed treap that supports reversal of subarrays. In node, s maintains the size of its subtree, and f is a flag that indicates reversal. We use push and pull to propagate data down and up the treap, respectively. Note the difference in the implementation of split. 62 Hsiang, Wei, Liu 1 2 3 4 5 Chapter 5. Complex Ideas and Data Structures struct node { node *l , * r ; int v , p , s , f ; node ( int v ) : l (0) , r (0) , v ( v ) , p ( rand () ) , s (1) , f (0) {} }; 6 7 8 9 int size ( node * t ) { return t ? t -> s : 0; } 10 11 12 13 14 15 16 17 18 19 void push ( node * t ) { if ( t == NULL ) return ; if ( t -> f ) { swap ( t -> l , t -> r ) ; if ( t -> l ) t -> l -> f ^= 1; if ( t -> r ) t -> r -> f ^= 1; t -> f = 0; } } 20 21 22 23 24 void pull ( node * t ) { if ( t == NULL ) return ; t -> s = size ( t -> l ) + size ( t -> r ) + 1; } 25 26 27 28 29 30 31 32 void split ( node *t , node *& l , node *& r , int k ) { push ( t ) ; if ( t == NULL ) l = r = NULL ; else if ( k <= size ( t -> l ) ) split ( t -> l , l , t -> l , k ) , r = t ; else split ( t -> r , t -> r , r , k - size ( t -> l ) - 1) , l = t ; pull ( t ) ; } 33 34 35 36 37 38 39 40 void merge ( node *& t , node *l , node push ( l ) , push ( r ) ; if ( l == NULL || r == NULL ) t = l else if ( l -> p > r -> p ) merge ( l else merge ( r -> l , l , r -> l ) , t pull ( t ) ; } *r){ ? l : r; -> r , l -> r , r ) , t = l ; = r; 41 42 43 44 45 46 47 48 49 50 void reverse ( node *t , int l , int r ) { // Reverses the range [l , r ]. node *a , * b ; split (t , t , b , r + 1) ; split (t , t , a , l ) ; a -> f ^= 1; merge (t , t , a ) ; merge (t , t , b ) ; } 63 Hsiang, Wei, Liu 5.5.2 Chapter 5. Complex Ideas and Data Structures Tree Rotation The key idea behind all of these data structures is the use of the tree rotation, which is used to change the structure of the tree but will not change the order of the elements (inorder). Maintaining the order of the elements is important, of course, if we wish our tree to remain a binary search tree. Q P rotate right P A C Q A B B rotate left C Here, the triangles represent subtrees, as A, B, and C could very well have children of our own, but they are not shown in the diagram. Note that the inorder ordering of the nodes has not been changed. When we rotate right, we literally take the edge connecting P and Q and rotate it clockwise. Then P becomes the parent of Q where before P was the child of Q. However, this definition of rotation is somewhat cumbersome, as we have different terms for the highly symmetrical rotating right and rotating left. The key characteristic a rotation is we move the lower node up one level. Thus, I prefer to think of tree rotation as whatever tree rotation, either left or right, will rotate up a node. In the diagram, rotating right is analogous to rotating P up, and rotating left is analogous to rotating Q up. Rotating a node up will change the tree such that its former parent is now its child. The other notable change in the tree structure is the subtree associated with B passes between P and Q upon tree rotation. Finally, tree rotation can happen at any place in the tree, not just at the root. When we rotate P up, we must update the parent of Q to change its child to P . D D Q P rotate P P A C B Q A rotate Q E 64 E B C Hsiang, Wei, Liu Chapter 5. Complex Ideas and Data Structures Note that in this example, rotating up P decreases the total height of the tree. We want to somehow systematically rotate up nodes to accomplish this. The following data structures provide such a system. 5.5.3 Splay Tree Remember that rotating a node brings it one level closer to the root. The idea behind a splay tree is to apply a sequence of rotations that rotate a node all the way up to the root. The intuition for doing this is once we access a node, it is easy to perform other operations on it since it will simply be the root of the tree. It is not at all clear that rotating nodes to the root will somehow improve the average performance of the tree. That’s because rotating the normal way doesn’t! To improve the performance of the tree, we expect that our balancing procedure tends to decrease the height of the tree. Unfortunately, simply repeatedly rotating a node up the tree doesn’t exactly do that task. Consider a tree that looks liked a linked list, and rotating the nodes in the order A, B, C, D, as shown in the diagram. The height of the tree remains linear in magnitude, as at its smallest stage the tree is around n2 in height, so accessing elements remains O(n) on average. D C A rot. A B A B D C rot. B A C D C B rot. C B D D rot. D A C B A As discussed earlier, simply repeatedly applying the standard rotation clearly is not guaranteed to reduce the height of the tree on average. However, if we simply make one small change, magic happens. The trick of the splay tree is to rotate a node to the root in such a way that the tree has a tendency to decrease in height. We’ll use two compound rotations, and depending on the structure of the tree, we’ll use a different one accordingly, to rotate a node to the root. When a node is a left child and its parent is a left child, or the node is a right child and its parent is a right child, we first rotate up the parent, and then rotate up the node. This sequence of rotations is the only difference between splaying and rotating a node to the root using standard rotation. 65 Hsiang, Wei, Liu Chapter 5. Complex Ideas and Data Structures Z X rotate Y , rotate X Y D X A Y C A B Z B C D When a node is a left child and its parent is a right child, or the node is a right child and its parent is a left child, we rotate the node up twice, the normal way. Z X rotate X, rotate X Y D A Y X B A Z B C D C Finally, when a node’s parent is the root, so it has no grandparent, we simply rotate up the normal way. Rotating a node to the root in this way is called splaying the node. Thus, splaying a node brings it to the root of the tree. Splaying seems too simple to lead to an amortized O(log n) solution. To get a better idea of how splaying works, let’s see what it does to a tree that looks like a linked list. 66 Hsiang, Wei, Liu Chapter 5. Complex Ideas and Data Structures G A F F splay A E D D B C G E C B A Splaying here cuts the height of the tree in half – a huge improvement. Splaying is not guaranteed to decrease the height of the tree, and it is possible for a sequence of splays to even result in a linked-list-like structure. However, given a number m at least equal to the maximum number of nodes n ever in the tree, any sequence of m splays runs in O(m log n). Splaying is then O(log n), amortized.2 Then, whenever we access a node, even in insertion and deletion, we splay that node to the top. This makes access, insert, and delete all O(log n) amortized. Since splay trees need not satisfy a restrictive coloring, as red-black trees do, we have the freedom to completely change the structure of the tree at a whim. Recall how tedious the casework was to simply add or remove one node at a time in a red-black tree. Because the splay tree is simply a normal binary search tree that rotates nodes up upon access, we can detach an entire subtree from the tree and not worry about any properties of our tree no longer being satisfied. For this reason, we define the split and join functions for splay trees. Given two trees S and T , such that every element in S is less than every element in T , we join S and T into one tree by splaying the largest element in S, so that the root, which is now the largest element, has no right child. Then, we set its right child to the root of T , resulting in a single tree with elements from both S and T . Given a tree and a value v, we split the tree in two by splaying the greatest element not greater than v to the root and detaching the right subtree from the rest. This results in two trees, one containing all elements at most v, and the other containing all elements greater than v. 2 If you’re curious to see the proof of this, a rather in depth explanation can be found here: http://www.cs.cmu. edu/afs/cs/academic/class/15859-f05/www/documents/splay-trees.txt 67 Hsiang, Wei, Liu Chapter 5. Complex Ideas and Data Structures Both of these operations are O(log n) amortized. It is possible to implement insert and delete using split and join. Since a binary search tree stores an ordered set, it is incredibly useful for storing a dynamic list of elements where the length can change and elements need to be added or removed from any point in the list, not just the beginning or the end. Splay trees are incredibly useful because of the split and join operations, as they allow us to remove consecutive elements from the list represented by our tree. For example, to remove elements indexed in the range [i, j], we split at index i and split at index j + 1 to get three splay trees, one representing [1, i − 1], another [i, j], and the last [j + 1, n]. We can then merge the first and third trees together, to get two trees, one representing [i, j] and the other representing the original list with [i, j] removed. A similar procedure can insert a list within another list as as contiguous block. Both of these operations can be completed in O(log n) amortized using a splay tree. 5.5.4 Red-Black Tree Red-black trees are not very useful in the contest environment, but since nearly every implementation of BBST in standard libraries uses them, it may be useful to be familiar with the idea behind them. As stated earlier, the red-black tree represents the “just do it” approach. We want to somehow bound the maximum length from the root to any leaf node by applying a constraint on the tree. In this case, our constraint is a coloring of the nodes (red or black) such certain properties are satisfied. For the red-black tree, the only nodes storing data are non-leaf nodes, and all leaf nodes store “null.” The number of null nodes, then, is n + 1, where n is the number of non-null nodes. We do this so that any node storing useful information has two children. We want our tree to satisfy the following properties: 1. The root is black. 2. All leaf nodes (null) are black. 3. Every red node has two black children. Consequently, a red node has a black parent. 4. Any path from the root to a null node contains the same number of black elements. 68 Hsiang, Wei, Liu Chapter 5. Complex Ideas and Data Structures M D T A null Q K C null null G E L I null null null X R null null V Z null null null null null null null null Note that every path from the root to a null node contains four black nodes. The proof of O(log n) search follows immediately. The shortest possible path contains only black nodes, and the longest possible path contains black and red nodes alternating. Since the number of black nodes in both must be the same, any path is at most twice as long as any other path. As the number of nodes in the tree is 2n + 1, the number of black nodes m in any path is then bounded below by 22m − 1 ≥ 2n + 1 and above by 2m − 1 ≤ 2n + 1. Thus the height of the tree is on the order O(log n), and we are done. Thus if our tree maintains its red-black coloring and satisfies the necessary properties, we can guarantee that our tree is balanced. We then consider the two ways we change the state of the tree, insertion and deletion. We can insert and delete in the normal way, but we might need to make changes to the tree after that to restore the red-black properties. We do this through a small number of color flips and tree rotations, which we can handle through casework. Let’s handle insertion first. When we insert a node, it takes the place of a black null leaf node. To maintain property 4, we must color the new node red, as it has two black null children. However, we may have violated some of the other properties, specifically 1 and 3. We’ll call the new node N , its parent P , its uncle (parent’s sibling) U , and its grandparent G, if they exist. We consider the following five cases. 1. N is the root node. That is, N is the first node added to the tree. It is easy to just change the color of N to red to restore property 1, and we are done. 2. P is black. Then property 3 is not violated, and we are done. 69 Hsiang, Wei, Liu Chapter 5. Complex Ideas and Data Structures 3. P is red, and U is red (G and U exist since P cannot be the root, as the root is black). As P and U are red, G is black. We simply color P and U black and G red to restore property 3. G G P U P N U N 4. P is red, U is black, and N is on the same side of P as P is of G. G must be black. We rotate up P and color P black and G red. G P P U N G N U 5. P is red, U is black, and N is on the opposite side of P as P is of G. G must be black. We rotate up N , and this reduces to case 4. G P G U N N U P Thus after we insert, we can always restructure the tree using a constant number of operations to restore the necessary red-black tree properties. Now we need to work on deletion. Recall how deletion works on a normal binary search tree. If the node we need to replace has two children, we swap the node with the least element in its right subtree, which does not have two children. We then are able to remove that node more easily. 70 Hsiang, Wei, Liu Chapter 5. Complex Ideas and Data Structures We can do the same thing with the red-black tree. If a node has two non-null children, we swap its value with the least non-null node in its right subtree and remove that node. Thus we reduce the deletion problem to the case where the node we need to remove has at least one null child. Two cases are very easy. 1. The node we wish to remove is red. Then the node must have two null leaf nodes as children. Then we remove the node by replacing it and its children with a single null node. 2. The node is black and has a red child. We replace the node with its child and paint its child red. The remaining case is the node black with two black null children. We’ll first replace the node with a null node N . Then, all paths passing through N are one black node short compared to all other paths. We denote the parent of N as P , its sibling S, and its sibling’s children C and F , such that C is on the same side of S as N is of P , if they exist. That is, C is the “closer nephew” child, while F is the farther. We now describe a six-case balancing procedure on the black node N that fixes our problem. 1. N is the root. We are done, as every path possible must pass through N , so all paths are balanced. 2. P is red and S, C, and F are black. Then we simply trade the colors of P and S. The number of black nodes for paths passing through S stays the same, but the number of black nodes for paths passing through N increases, so we are done. P P N S C N F S C F 3. S is black and F is red. Then we color F black and rotate up S, before swapping the colors of P and S. Then paths passing through N gain a black vertex, while paths passing through C and F stay the same. 71 Hsiang, Wei, Liu Chapter 5. Complex Ideas and Data Structures P S N S C P F N F C 4. S is black, F is black, and C is red. Then we rotate up C and swap the colors of S and C. Then this reduces to case 3. P P N S C N C F S F 5. S is red. Then P , C, and F must be black. We rotate S up and swap the colors of P and S. C, the new sibling of N is black, so this then reduces to one of cases 2, 3, or 4. P S N S C P F N F C 6. P , S, C, and F are all black. Then we recolor S red. Now, all paths through P have the same number of black nodes, but each path through P has one less black node than each path not passing through P . Since P is black, and this procedure did not require that N be a leaf node, we can repeat this entire balancing procedure on P . 72 Hsiang, Wei, Liu Chapter 5. Complex Ideas and Data Structures P P N S C N F S C F Unlike the balancing for insertions, the balancing for deletions has the potential to call itself on the parent node P . However, balancing following a deletion is still O(log n) worst case. It is possible prove that it is O(1) amortized. Regardless, we now have a deletion algorithm that runs in O(log n) time. Thus the red-black tree supports standard binary search tree operations, all in O(log n). 73 Chapter 6 Computational Geometry For actual geometry problems, and not graph theory problems hiding in the plane. I’m too lazy to actually write this section right now so here are some useful links from the USACO Training Pages. https://www.dropbox.com/s/nqzk63bjby1iaq9/Computational%20Geometry.pdf?dl=0 https://www.dropbox.com/s/ykf65dk6sefb6zk/2-D%20Convex%20Hull.pdf?dl=0 These essentially cover anything I would want to say in this chapter anyway, so I’ll likely fill this chapter out last. 75 Hsiang, Wei, Liu 6.1 Chapter 6. Computational Geometry Basic Tools Cross Product Dot Product tan−1 , atan2 6.2 Formulas 6.2.1 Area 6.2.2 Distance 6.2.3 Configuration 6.2.4 Intersection 6.3 Convex Hull 6.4 Sweep Line Oftentimes when trying to solve geometry problems, or combinatorial problems that we give a geometric interpretation, we have something that involves one or two dimensions. If this is difficult to deal with directly, we can use a sweep line to reduce the dimension of the problem. Sweep lines are best explained with an example. Suppose we’re given several intervals on the number line, and we want to find the length of their union. If we wanted to, we could use a segment tree with lazy propagation to insert the intervals and calculate the total length. However, a sweep line gives us a much easier approach. Imagine a vertical beam of light sweeping from −∞ to ∞ on the number line. At any given time, we keep track of the number of intervals that are hit by this beam. This takes the problem from one dimension to zero dimensions. Our answer is then the length over which our beam hit a positive number of intervals. Our implementation of a sweep line simulates this process. If we sort the endpoints of our intervals by their positions on the number line, we can simply increment our counter each time we encounter a “start” endpoint and decrement our counter each time we encounter an “end” endpoint. We can look at the regions between consecutive endpoints, and add to our result if our counter is positive in that region. 76 Hsiang, Wei, Liu Chapter 6. Computational Geometry One other way to interpret sweep lines is to consider time as the dimension we sweep along. For the example above, this would mean that each interval appears and then disappears on our beam, existing only when the time is between its endpoints. Although time may not seem useful for our one dimensional problem, this type of thinking helps in higher dimensions. Most sweep lines that you use won’t be as simple as this. Sweep line problems usually involve reducing a two dimensional problem to a one dimensional problem and require maintaining data structures such as BBSTs or segment trees along that dimension. This technique also generalizes to higher dimensions—to solve a three dimensional problem, we can sweep along one dimension and use two dimensional data structures to maintain the other two dimensions. To finish, let’s go over another example, Cow Rectangles from USACO 2015 January: The locations of Farmer John’s N cows (1 ≤ N ≤ 500) are described by distinct points in the 2D plane. The cows belong to two different breeds: Holsteins and Guernseys. Farmer John wants to build a rectangular fence with sides parallel to the coordinate axes enclosing only Holsteins, with no Guernseys (a cow counts as enclosed even if it is on the boundary of the fence). Among all such fences, Farmer John wants to build a fence enclosing the maximum number of Holsteins. And among all these fences, Farmer John wants to build a fence of minimum possible area. Please determine this area. A fence of zero width or height is allowable. The first observation we should make is that we want a Holstein on every side of the fence. Otherwise, we would be able to decrease the area of our fence without decreasing the number of Holsteins. Even with our observation, this problem still seems hard to deal with. We can make it easier by adding another constraint: Suppose we knew which Holstein was on the leftmost boundary of our fence. Since we added the constraint to the x-dimension, we naturally want to figure out where our rightmost Holstein is next. We can do this with a sweep line moving to the right. The reason we want a sweep line here is because for any given rightmost cow, we want to know information about the Guernseys and Holsteins in between. With a sweep line, we can collect and maintain all this data by processing the cows from left to right. For example, whenever we see a Guernsey, we have to limit the y-coordinates that our Holsteins can take. And whenever we see a Holstein, we have to consider this Holstein as our rightmost cow and also store it in case we include it in a fence later on. What remains is to find an appropriate data structure to track all this. A set data structure (STL set or Java TreeMap) turns out to be enough. We can insert the Holsteins, sorted by y-coordinate, and delete one whenever any rectangle bounded by that Holstein, the leftmost Holstein, and our 77 Hsiang, Wei, Liu Chapter 6. Computational Geometry sweep line includes a Guernsey. Thus we take O(n log n) time to sweep, giving us an O(n2 log n) solution overall. This type of analysis is pretty representative of sweep line problems. Whether you’re given rectangles, line segments or even polygons, you want to think about how you can reduce the dimension and obtain a tractable problem. Note that sweep lines don’t always have to move along some axis. Radial sweep lines (sweeping by rotating) and sweeping at an angle (rotating the plane by 45◦ ) also work. 78 Chapter 7 Tree Algorithms Up until now, we have only looked at algorithms that deal with general graphs. However, there is also much to be said about graphs with additional structure. In this section, we’ll explore some problems and their solutions on trees. First, some definitions. An undirected graph G is a tree if one of the following equivalent conditions holds: • G is connected and has no cycles. • G is connected with n vertices and n − 1 edges. • There exists exactly one simple path between any two vertices of G. A rooted tree is a tree where one vertex has been designated as the root. This gives each edge a natural direction – whether it leads towards or away from the root. In many problems, it is useful to arbitrarily designate a root. For example, one interpretation of a DFS on a tree is that we start from the root and traverse the tree downwards towards the leaves. (On trees, a leaf is a vertex with degree 1.) We’ll define a few more terms for rooted trees. An ancestor of a vertex v is another vertex a that lies on the path between v and the root. The parent of a vertex is its closest ancestor. The depth of a vertex is its distance from the root. We will use depth(v) to denote the depth of a vertex v. 7.1 DFS on Trees DFS is a very useful technique for collecting information about the structure of a tree. For example, we can compute in linear time the depth of each node, the size of each subtree or the diameter of the entire tree. This can be done by recursively computing results for each subtree, and then combining this data to obtain the final result. Sometimes, it takes more than one DFS to collect all the necessary information. Calculating node depth and subtree size via DFS is relatively straightforward. Below is a snippet of pseudocode computing these two sets of values. 79 Hsiang, Wei, Liu Chapter 7. Tree Algorithms function DFS(v, p) . v is the current vertex, p is its parent sum(v) ← 1 . sum(v) is size of the subtree rooted at vertex v depth(v) ← depth(p) + 1 . depth(v) is the depth of vertex v for all vertices n adjacent to v do if n 6= p then DFS(n, v) sum(v) ← sum(v) + sum(n) Computing the diameter of a tree is a bit trickier. For a given tree, let r be its root, and let a be a vertex of maximum depth. It turns out that the diameter of the tree is the maximum distance between a and any other vertex in the tree. (Try to prove this!) Thus we can run two DFSes to compute the diameter, calculating the depth of each vertex with each pass. Depth, subtree size and diameter are only a few of the values we can compute for trees. This style of DFS shows up often in problems and is also fundamental to many more complex tree algorithms. 7.2 Jump Pointers Jump pointers are a technique for decomposing paths on rooted trees. The central idea is that we can always take a path and break it into O(log n) chunks of size 2i . By augmenting these smaller chunks with data, we can use jump pointers to quickly answer a variety of queries, including level ancestor and lowest common ancestor. Let’s start with an example. The most fundamental application of jump pointers is to level ancestor queries—for any vertex v, we want to be able to find its kth ancestor in O(log n) time.To solve this problem, we precompute for each vertex a pointer to its 2i th ancestor for all possible values of i. These are the “jump pointers” for which this technique is named. If our tree has n vertices, then each vertex has at most blog2 nc pointers leading out from it. Thus we have at most O(n log n) jump pointers in total. We can compute these with a DP in O(n log n). Using jump pointers, we can finish the level ancestor problem. Let 2i be the largest power of 2 that is at most k. We use our jump pointers to obtain u, the 2i th ancestor of v. If 2i is not equal to k, we can jump up again from u. Continuing recursively, we’ll finish in O(log n) iterations, since we reduce k by at least a factor of 2 each time. Now we’ll tackle lowest common ancestor queries. The lowest common ancestor (LCA), l, of two vertices u and v is the deepest vertex that is an ancestor of both u and v. LCA queries are especially useful because any path on a rooted tree can be broken into two shorter paths joined at the LCA of its endpoints. The solution of the LCA problem with jump pointers consists of two steps. The first step involves bringing the two queried vertices to the same depth. If we are looking for the LCA of u and v, we can assume that depth(u) > depth(v) without loss of generality. Let u0 be the ancestor of u satisfying depth(u0 ) = depth(v). We can compute u0 with a single level ancestor query in O(log n) time. If u0 = v, then we are done. Otherwise, if u0 6= v, we can find the LCA of u0 and v by advancing them towards the root in increments of 2i in a binary-search-like manner. If the 2i th ancestors of u0 and v are distinct, then the LCA of u0 and v is equal to the LCA of the 2i th ancestors of u0 and v. Thus, iterating down 80 Hsiang, Wei, Liu Chapter 7. Tree Algorithms from the largest power of 2 less than n, we can move u0 and v up the tree until they share a parent. This common parent is the LCA of u and v. Since jump pointers allow us to access the 2i th ancestor of a vertex in O(1), our algorithm runs in O(log n) time. Algorithm 8 Jump Pointers, Level Ancestor and LCA function BuildJumpPointers(V , par) . Initially, par(0, v) holds the parent of vertex v. i par(i, v) denotes the 2 th ancestor of vertex v for all i from 1 to blog2 nc do for all vertices v do par(i, v) ← par(i − 1, par(i − 1, v)) function LevelAncestor(u, k) for all i from blog2 nc to 0 do if 2i ≤ k then u ← par(i, u) k ← k − 2i return u function LCA(u, v) if depth(u) < depth(v) then swap(u, v) u ← LevelAncestor(u, depth(v) − depth(u)) if u = v then return u for all i from blog2 nc to 0 do if par(i, u) 6= par(i, v) then u ← par(i, u) v ← par(i, v) return par(0, u) Level ancestor and LCA are the two basic tools to jump pointers. Applying these, we can quickly answer queries about paths, such as the length of the path between two vertices u and v. If we augment the jump pointers by storing additional information, we can also compute maximum weight or distance queries on weighted trees. Another important observation that we can make is that we can compute jump pointers on the fly, adding edges and answering queries online. We now take some time to acknowledge a few limitations of jump pointers. Because each vertex of the tree is covered by O(n) jump pointers on average (including the ones jumping over it), this structure cannot handle updates efficiently. Thus we usually apply jump pointers only if we know that the weights/values stored with the jump pointers will not change after being computed. For example, if we want to both answer maximum weight queries for paths and update edge weights, we should use heavy-light decomposition or link-cut trees to do so. Overall, however, jump pointers are still a very flexible technique. With some creativity, these ideas can be used to provide elegant and easy-to-code solutions for problems that would otherwise require more complex data structures. 81 Hsiang, Wei, Liu 7.3 Chapter 7. Tree Algorithms Euler Tour Technique The Euler tour technique is a method of representing a tree (not necessarily binary) as a list. The idea is that a tree is uniquely determined by its DFS traversal order, going both down and up the tree. We’ll keep track of the order in which we traverse the edges. Since each edge is traversed twice, each edge will appear in our list twice. We’ll also name each edge by the child vertex in the parent-child pair. A 1 16 b d c 10 9 8 11 B C D 12 e 7 2 15 f g 13 14 E F 3 G 6 h i 4 5 H I The edge traversal order is then described by the ordered list below. b1 e1 h1 h2 i1 i2 e2 b2 c1 c2 d1 f1 f2 g1 g2 d2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 We see a pattern in this list – the subtree of a node is contained between the two edges representing that node in the list, inclusive. For example, from the first e to the second e, the entire subtree of E is contained within the range [2, 7]. 7.3.1 Euler Tour Tree The Euler tour tree uses this idea. Because we see the relationship between the list and the subtrees of the tree, we’ll just have the list store the vertex names. To complete this pattern for the subtree of A, which is the entire tree, we’ll simply add that letter to the beginning and end of the list. 82 Hsiang, Wei, Liu A1 B1 E1 1 2 3 Chapter 7. Tree Algorithms H1 H2 4 I1 I2 E2 B2 C1 C2 D1 F1 F2 G1 G2 D2 A2 6 7 8 9 10 11 12 13 14 15 16 17 18 5 Now suppose we wanted to perform some operations on a forest, or collection of trees. The first operation is to find the root of the tree containing a vertex v. The second is to link the trees of two vertices u, v together by making v the parent of u. The third is to cut the subtree of v away from the rest of the tree by deleting the edge from v to its parent. Note that, without the cut operation, this is simply union-find. However, because of the cut operation, we have to maintain the structure of the original forest, as otherwise we lose track of the actual parent of any given node. We see that all operations are not simply on a single vertex but on an entire subtree. However, if we look at what each of the operations does to the Euler tour lists representing our forest, find returns the first element in the list, link places one ordered list within another, and cut removes a large contiguous section of one ordered list. To cut E from the tree, we need to split the list in two: A1 B1 E1 H1 H2 1 2 I2 6 7 8 E1 H1 H2 I1 I2 E2 3 6 7 8 3 A1 B 1 1 I1 2 4 5 4 5 E2 B2 C1 C2 D1 F1 F2 G1 G2 D2 A2 9 10 11 12 13 2 3 4 5 6 7 8 15 16 17 18 B2 C1 C2 D1 F1 F2 G1 G2 D2 A2 9 10 11 12 A1 B1 B2 C1 C2 D1 F1 F2 G1 G2 D2 A2 1 14 9 10 11 12 13 14 15 16 17 E1 H1 H2 I1 I2 E2 1 4 5 6 2 3 18 Let’s see what happens when we link E to D. We need to split the first list immediately after D1 . A1 B1 B2 C1 C2 D1 F1 F2 G1 G2 D2 A2 1 2 3 4 5 6 7 A1 B1 B2 C1 C2 D1 1 2 12 1 4 5 6 2 3 F1 F2 G1 G2 D2 A2 E1 H1 H2 I1 I2 E2 7 1 4 5 6 12 A1 B1 B2 C1 C2 D1 E1 H1 H2 I1 I2 E2 F1 F2 G1 G2 D2 A2 10 11 12 4 6 11 E2 11 3 5 10 I2 10 2 4 9 I1 9 1 3 8 E1 H1 H2 5 6 7 8 8 9 13 14 2 15 3 16 17 18 We have a data structure that maintains a set of ordered elements that can split and merge quickly. Splay trees can maintain an ordered list, and the split and join operations on splay trees can easily implement link and cut. Each tree of our forest is then represented by a splay tree maintaining that tree’s Euler tour list, and we can support each of the three necessary operations in O(log n) amortized time. 83 Hsiang, Wei, Liu 7.4 Chapter 7. Tree Algorithms Heavy-Light Decomposition Brute-force algorithms on trees are often fast when the tree is nearly complete, or has small depth. Unfortunately, brute force is far too slow when the tree becomes more and more unbalanced and approximates a linked list. Certain problems can be solved very nicely in a list with segment trees and similar data structures. Heavy-light decomposition provides us with a way to exploit the fact that long “chains," which are present in unbalanced trees, might be slow in brute force but can be sped up with other data structures. Heavy-light decomposition provides us a way to dynamically find the lowest common ancestor as well as a way to prove time complexities about tree data structures, like link-cut trees. Heavy-light decomposition is a coloring of all the edges in a binary tree either heavy or light. For each vertex v, let s(v) denote the number of nodes in the subtree with v as its head. Then, if u, w are the children of v, s(v) = s(u) + s(w) + 1. We see that the smaller child u or w must have less than half the subtree size as v, and so that child exhibits qualities similar to those of a completely balanced tree. We color the edge connecting v to its lesser child light. We color the other edge heavy. We see that from any node, the number of light edges needed to reach the head of the tree is at most log n, as each light edge doubles the subtree size. Then, the number of “heavy chains" along the upwards path from any node is also of the order log n. 17 12 6 3 1 7.5 2 1 4 5 1 1 1 3 2 1 Link-Cut Tree 84 1 1 Chapter 8 Strings 8.1 String Hashing String hashing is a probabilistic technique for dealing with strings. The most common hashing algorithm used in programming contests is the polynomial hash, also known as the Rabin-Karp hash. This method allows for the fast comparison of strings and their substrings—O(1) after linear time precomputation. Thus we can often use conceptually simpler hashing to replace trickier string algorithms, such as Knuth-Morris-Pratt and the Z-algorithm. In addition, adding binary search allows for the computation of the longest common prefix in log n time, useful in constructing suffix arrays. Jonathan Paulson gives a great introduction to polynomial hashing: https://www.reddit.com/ r/usaco/comments/39qrta/string_hashing/ 8.2 Knuth-Morris-Pratt Knuth-Morris-Pratt is an easy-to-code linear time string matching algorithm. Using the “needle and haystack” analogy, for a given needle of length n and a haystack of length m, KMP takes O(n) to preprocess the needle and O(m) to search. Hariank and Corwin explain the algorithm well: https://activities.tjhsst.edu/sct/lectures/1415/stringmatching_10_3_14.pdf Beyond string matching, KMP is also useful for problems involving periodic strings. This is because a string with an equal prefix and suffix of length l has a period of n − l, which is exactly what KMP computes. A similar (and equivalent) algorithm is the Z-algorithm, explained here: http://codeforces. com/blog/entry/3107 8.3 Trie A trie (from retrieval) is a data structure for storing strings that supports insertion and look-up in linear time. The trie maintains the strings in a rooted tree, where each vertex represents a prefix and each edge is labeled with a character. The prefix of a node n is the string of characters on the 85 Hsiang, Wei, Liu Chapter 8. Strings path from the root to n. (In particular, the prefix of the root is the empty string.) Every string that is stored in the tree is represented by a path starting from the root. Below is a picture of a trie storing “COW,” “MO,” “MOM,” “MOO,” and “MOP.” C M C M O O CO MO $ W COW $ COW$ MO$ M MOM $ MOM$ O P MOO $ MOO$ MOP $ MOP$ Insertion into a trie is straightforward. We start at the root, and create edges and vertices as necessary until a path representing the string is formed. If a vertex already has an edge of the correct character leading out from it, we just travel down that edge. In order to identify the end of a string, we can append a “$” to every string before we insert. Searching is the same as insertion, except we terminate our search when we can’t go any further instead of adding a new edge. What we’ve seen so far doesn’t make the trie seem particularly useful—string insertion and look up can be easily done in linear time with Rabin-Karp and a hash table as well. The advantage of the trie comes from its tree structure. Having common prefixes bundled together on the same path means we can compute compute information relating to prefixes easily. This allows for tree DPs that we wouldn’t be able to do with hashing. (However, hashing does handle certain forms of prefix queries well, such as longest common prefix.) Tries are also a building block for other string data structures, such as the Aho-Corasick automaton and the suffix tree. (In fact, tries can be viewed as a very simple form of string automaton.) 8.4 Suffix Array For a string of length n, each index i (0 ≤ i < n) can represent the suffix that starts at that index. A suffix array is a list of these indices, sorted in lexicographically increasing order by suffix. Thus a suffix array is essentially a sorted list of the suffixes of a string. Below is the suffix array of “mississippi$.” 86 Hsiang, Wei, Liu Chapter 8. Strings Suffix Array Suffix 11 $ 10 i$ 7 ippi$ 4 issippi$ 1 ississippi$ 0 mississippi$ 9 pi$ 8 ppi$ 6 sippi$ 3 sissippi$ 5 ssippi$ 2 ssissippi$ At first, it might seem surprising that such a structure is useful—why do we care about suffixes at all? The crucial observation is that every substring of a string is a prefix of a suffix. Thus if we have something that does well with prefixes, such as hashing or a trie, we use this to compute information about substrings. A trie built from suffixes is known as a suffix tree, which we’ll cover later. In this section, we’ll go over what we can do with hashing and a suffix array. First, let’s figure out how we can construct a suffix array. The naive solution is to sort all n suffixes using O(n) string comparison. Since sorting itself takes O(n log n) comparisons, we have an O(n2 log n) algorithm. However, with hashing and binary search, we can lexicographically compare two strings in O(log n) time. We do this by binary searching for the longest common prefix and then comparing the next character. We can can compute the hash of any substring with a polynomial hash, so it’s easy to compare if the prefixes of two suffixes (a.k.a two substrings) are equal. Now that we have O(log n) comparison, our algorithm runs in O(n log2 n) overall. Note that it is also possible to compute suffix arrays in O(n log n) and even O(n), but these algorithms are much more involved. For the purposes of contests, an O(n log2 n) suffix array algorithm should almost always be enough. With a suffix array, we can check if any queried “needle” string exists using a binary search with hashing. We can even count the number of occurrences by binary searching for the first and last match. This works in O(m + log n log m), where m is the length of the needle, because we need O(log m) operations for string comparison and O(log n) iterations of the binary search. Suffix arrays differ from KMP because KMP preprocesses the “needle,” while suffix arrays preprocess the “haystack.” From the suffix array, we can also obtain another useful data structure called the LCP array. This array stores the longest common prefix between adjacent suffixes in the suffix array. We can use it to speed up pattern matching to O(m + log n). In addition, we can build a segment tree over the LCP array to answer queries, such as the LCP between two arbitrary suffixes. 87 Hsiang, Wei, Liu Chapter 8. Strings Suffix arrays can also be used to solve many string problems beyond matching. This data structure does very well with most problems involving substrings. For example, suffix arrays/LCP arrays can count the number of distinct substrings in a string or return the kth lexicographically largest substring. The minimum string rotation problem can is another problem solvable with suffix arrays, since each rotation of a string S is a substring of S + S. To handle multiple strings with suffix arrays, we can either concatenate them with separator characters in between or separately compute their hashes. Below is a short C++ code for computing suffix arrays. Look up lambda functions in C++ if you’re not familiar with the syntax. 1 2 3 4 typedef unsigned long long ull ; // We use A as the exponent and M as the mod for our hash . const ull M = 1000000007; const ull A = 127; 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 void build ( string S , int * sa ) { // Builds a suffix array for a string S in array sa . S . push_back ( ’$ ’) ; int N = S . size () ; ull P [ N + 1] , H [ N + 1]; P [0] = 1; // P stores the precomputed powers of A . H [0] = 0; // H stores the prefix hashes of S . for ( int i = 0; i < N ; i ++) { // Precomputing powers and hashes . P [ i + 1] = A * P [ i ] % M ; H [ i + 1] = ( A * H [ i ] + S [ i ]) % M ; sa [ i ] = i ; } auto f = [&]( int a , int l ) { // Returns the hash of the substring starting at index a with length l . return ( H [ a + l ] - H [ a ] * P [ l ] % M + M ) % M ; }; auto comp = [&]( int a , int b ) { // Compares the suffixes starting at indices a and b . int lo = 0 , hi = min ( N - a , N - b ) ; while ( lo + 1 < hi ) { int m = ( lo + hi ) / 2; ( f (a , m ) == f (b , m ) ? lo : hi ) = m ; } return S [ a + lo ] < S [ b + lo ]; }; sort ( sa , sa + N , comp ) ; } 8.5 Aho-Corasick One way to think about the Aho-Corasick algorithm is KMP/Z-algorithm on a trie. This algorithm is used for matching multiple needles in a single haystack and runs in time linear in the length of the haystack, with preprocessing linear in the total length of the needles. To do this, we first add all strings to a trie that stores some extra information at its nodes. Upon inserting each string, we mark the last node visited as a endpoint node. Each node, in addition to storing its children and its 88 Hsiang, Wei, Liu Chapter 8. Strings status as an endpoint node, stores a suffix link. This link points to the longest proper suffix that is also a node on the trie. If necessary, each node can also store a dictionary suffix link, which points to the first endpoint node reachable by only following suffix links. To build an Aho-Corasick automaton, we start with a trie of the needle strings. What we want to do is compute the suffix links. We simplify this by using an O(nk) approach, where k is the alphabet size. For each node n, we compute an additional failure function, with one value corresponding to each letter of the alphabet. Suppose p is the prefix represented by n. Then the failure function of n for the letter α points to the node representing longest suffix of p + α in the trie. We can compute all of this with a BFS. When we are at a node, we can find the suffix links of its children using the failure function of its own suffix link. We also have to calculate the node’s own failure function. For every letter that has a corresponding child, its failure function is equal to that child. Otherwise, its failure function is equal to the corresponding failure function of the node’s suffix link. Take a moment to think through why this works. To query, we can iterate through our haystack string character by character and make the corresponding moves in the automaton. We follow the failure function when no child exists. However, note that there isn’t really a distinction between a normal trie edge and a failed edge anymore. To check for matches, we can look at the values we store at each node and/or follow dictionary suffix links. To find all matches with dictionary suffix links, we have to follow the pointers until we reach a node that doesn’t have a suffix in the dictionary. Note that if we follow dictionary suffix links, the complexity of our algorithm will also be linear in the number of matches. Here’s an implementation of Aho-Corasick in C++ without dictionary suffix links: 89 Hsiang, Wei, Liu 1 2 3 4 5 6 7 8 9 10 Chapter 8. Strings const int SIGMA = 26; const int MAXN = 100005; // Each node is assigned an index where its data is stored . // ch first stores the children of each node and later the failure function . // val counts the number of strings ending at a given node . // link stores suffix links . int ch [ MAXN ][ SIGMA ] , val [ MAXN ] , link [ MAXN ] , sz = 1; // Array q is a hand - implemented queue . // h and t point to the head and tail , respectively . int q [ MAXN ] , h , t ; 11 12 13 14 15 16 17 18 19 20 21 22 void add ( string s ) { // Adds a string to the trie . int p = 0; // p tracks our current position and starts at 0 , the root . for ( int i = 0; i < s . size () ; i ++) { int c = s [ i ] - ’A ’; // ch [ p ][ c ] is 0 if null , so we have to allocate a new node / index . if (! ch [ p ][ c ]) ch [ p ][ c ] = sz ++; p = ch [ p ][ c ]; } val [ p ]++; // Updates endpoint marker . } 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 void build () { // Computes all suffix links with a BFS , starting from the root . h = t = 0; q [ t ++] = 0; while ( h < t ) { int v = q [ h ++] , u = link [ v ]; val [ v ] += val [ u ]; // Propagates endpoint marker . for ( int c = 0; c < SIGMA ; c ++) { if ( ch [ v ][ c ]) { // If child exists , we create a suffix link . // The node ’s failure function here is the child . // Also , note that we have a special case if v is the root . link [ ch [ v ][ c ]] = v ? ch [ u ][ c ] : 0; q [ t ++] = ch [ v ][ c ]; } else { // Otherwise , we need to figure out its failure function . // We do this by using the failure function of the node ’s suffix link . ch [ v ][ c ] = ch [ u ][ c ]; } } } } 8.6 Advanced Suffix Data Structures I’ve yet to see these used in a contest, but they do exist. 90 Hsiang, Wei, Liu 8.6.1 Suffix Tree 8.6.2 Suffix Automaton Chapter 8. Strings 91 Chapter 9 More Graph Algorithms With the renewal of strongly connected components and network flow in the IOI syllabus, we overview some more graph algorithms that are standard but also noticeably more difficult than other algorithms presented in earlier chapters. 9.1 Strongly Connected Components This section covers Tarjan’s algorithm detects strongly connected components in a directed graph. We begin with the DFS traversal of the graph, building a forest (collection of trees). In a DFS, we only traverse each vertex once; this means when we encounter an edge to a vertex we already visited, we move on without calling the DFS on that node. Once we obtain the forest, we can split it into subgraphs that represent our strongly connected components. Let’s work through how we can get the subgraphs representing the strongly connected components from our DFS. Here’s the graph from the beginning of this section. 1 2 3 4 5 6 7 8 It doesn’t matter from which node we begin our DFS or the order in which we choose children; in our example, we’ll simply choose the node with the minimum label to traverse first. This will result in two trees in our forest, one rooted at 1 and the other rooted at 8. 93 Hsiang, Wei, Liu Chapter 9. More Graph Algorithms 1 8 2 3 5 6 7 4 Now this by itself is not very useful, as in order to find strongly connected components, we’ll need the edges in the directed graph that aren’t included in our tree. Here we add the other edges as dashed pointers. 1 8 2 3 5 6 7 4 Note that strongly connected components are represented by subtrees in the graph. We call the root of the subtree called the root of the strongly connected component. One edge here is immediately useless. We already know that 2 can reach 7; 7 is in 2’s subtree. The fact that there is an edge from 2 to 7 doesn’t change anything. Then we have a crucial observation – the only possible useful extra edges are those that go up to a previous node in the subtree, like the edge from 5 to 1, or those that go “left” to a previously-visited vertex, either to a previous branch in the tree, like from 7 to 3, or to a previous subtree entirely, like from 8 to 4. If a node v has an edge to a direct ancestor in the tree, that means we immediately have a cycle, and therefore the node, its ancestor, and every vertex along the way must be in the same strongly connected component. 94 Hsiang, Wei, Liu Chapter 9. More Graph Algorithms Naturally, the “left” case is trickier. Suppose v has a left edge to a vertex u. We somehow need a way to find out if u has a path back to v. We know that u cannot go down the tree to v as v is not in the subtree of u by the way DFS constructed the tree. Therefore, we want to know whether u has a path back up to some common ancestor with v. However, again by the way DFS traverses the graph, the entire subtree of u has already been searched before the DFS reaches v. We want to exploit this fact with some kind of memoization. If vertex v was the nth vertex visited in the DFS, we’ll mark v with the label order(v) = n. We’ll also keep track of the “least” vertex link(v) = u that we know up to that point that v can visit, or the vertex u with the minimum order(u) that v can reach so far. As we’re using a DFS, we’ll use a stack S to keep track of nodes we’ve visited. In a normal DFS on a tree, once we finish exploring a vertex v, we pop off v from the stack. This will not be the case for us. A node remains on the stack iff it has a path to some node earlier in the stack. This means as we explore the descendants of a vertex v, we’ll know if v has a path back to a previous vertex. That is, if link(v) < order(v), it stays on the stack. If link(v) = order(v), we take it off the stack. Now we describe Tarjan’s algorithm. Here, num represents a global variable that indicates how many vertices have been visited so far. Algorithm 9 Tarjan function StrongConnect(vertex u) num ← num + 1 order(u) ← num link(u) ← order(u) push u on S for all neighbors v of u do if order(v) is undefined then StrongConnect(v) link(u) ← min(link(u), link(v)) else if v is on stack S then link(u) ← min(link(u), order(v)) if link(u) = order(u) then create new strongly connected component repeat v ← top of S add v to strongly connected component pop top from S until u = v function Tarjan(G(V, E)) num ← 0 initialize new empty stack S for all vertices v ∈ V do if order(v) is undefined then StrongConnect(v) This algorithm runs in O(V + E). 95 . increment num . set order(u) to smallest unused number . least order(v) accessible is u itself . v has not been visited . v is in current component . u is root of component, create SCC . v has not been visited Hsiang, Wei, Liu 9.2 Chapter 9. More Graph Algorithms Network Flow We are given a directed graph with weighted edges, a source node, and a sink node. A flow is sent from the source to the sink. Each edge weight represents the maximum capacity of that edge. For every node besides the source and the sink node, total flow in is equal to total flow out. We can think of a flow network as a series of pipes through which water travels from an entrance to an exit in the network. The edge capacities represent pipe thickness. At any node, the total rate at which water enters the node must equal the total rate at which it exits the node, and along any path, the rate at which water flows is bottlenecked by the thinnest pipe. More formally, for a graph G(V, E), where c(u, v) represents the capacity of the edge from u to v, the flow f (u, v) from a source s to a sink t satisfies X f (u, v) ≤ c(u, v) ∀(u, v) ∈ E f (u, v) = −f (v, u) ∀(u, v) ∈ E f (u, v) = 0 ∀u ∈ V \ {s, t} v∈V X f (s, v) = v∈V X f (v, t) = |f |, v∈V where |f | represents the total flow from the source to the sink. A maximum flow is one that maximizes the total flow |f | from s to t, or in our example, maximizes the rate at which water can flow through our network. We’ll also define the residual capacity cf (u, v) = c(u, v) − f (u, v). Note that cf (u, v) ≥ 0 by the conditions imposed on f . The residual capacity of an edge represents how much capacity is left after a certain amount of flow has already been sent. We therefore have the residual graph Gf (V, Ef ), where Ef is the graph of residual edges, or all edges (u, v) ∈ V 2 satisfying cf (u, v) > 0. A natural approach to “solving” this problem would be to simply greedily add flow. Find a path from the source to the sink in which all the edges have positive weight in the residual graph. Send flow along this path; that is, find the max flow across this path, which is the minimum weight of any edge on this particular path. Call this value cap. Then subtract cap from the residual capacity of every edge in the path. We repeat, and this is guaranteed to terminate since on any given move, we remove an edge from our residual graph. What is wrong with our greedy approach? Consider the following graph: 2 2 1 1 2 1 3 4 2 The max flow from vertex 1 to vertex 4 is 3, but greedy gives only 2. This is because the best possible single path from the source to the sink may not be included the best possible overall flow. 96 Hsiang, Wei, Liu 9.2.1 Chapter 9. More Graph Algorithms Ford-Fulkerson We somehow need a way to fix the inclusion of any suboptimal paths in our greedy approach, or to “send flow back” in case we sent it through a suboptimal path. We do this by introducing the reverse edge to our residual graph. Find a path from the source to the sink in which all the edges have positive weight in the residual graph. Find the max flow across this path, which is the minimum weight of any edge on this particular path. Call this value cap. Then subtract cap from the residual capacity of every edge along the path and increment the residual capacity of the reverse edge (the edge connecting the same two vertices but running in the opposite direction) by cap. We call this operation on the path augmenting the path. We simply choose an augmenting path until no such paths exist. Algorithm 10 Ford-Fulkerson function AugmentPath(path p = {vi }m i=1 , where (vi , vi+1 ) ∈ Ef , v1 = s, vm = t) cap ← minm−1 (c (v , v )) f i i+1 i=1 for i ≡ 1, m − 1 do f (vi , vi+1 ) ← f (vi , vi+1 ) + cap cf (vi , vi+1 ) ← cf (vi , vi+1 ) + cap f (vi+1 , vi ) ← f (vi+1 , vi ) − cap cf (vi+1 , vi ) ← cf (vi+1 , vi ) + cap . incrementing reverse edge function MaxFlow(G(V, E), s, t ∈ V ) for all (u, v) ∈ V 2 do f (u, v) ← 0 cf (u, v) ← c(u, v) |f | ← 0 while ∃p = {vi }m i=1 , where (vi , vi+1 ) ∈ Ef , v1 = s, vm = t do m−1 cap ← mini=1 (cf (vi , vi+1 )) |f | ← |f | + cap AugmentPath(p) return |f | The difference between this algorithm and the greedy approach from earlier is that the paths we now allow may run along a reverse path, essentially undoing any suboptimal flow from earlier. These more general paths in our residual graph are called augmenting paths. This algorithm is guaranteed to terminate for graphs with integral weights. Its performance is bounded by O(Ef ), where f is the maximum flow and E is the number of edges, as finding a path from s to t takes O(E) and increments the total flow f by at least 1. The concept of removing edges can’t be used to produce a stricter bound because while an edge in one direction may be removed from the residual graph, doing so creates an edge in the other direction. In its crudest form, Ford-Fulkerson does not specify on which path to push flow if multiple paths exist. It simply states that as long as such a path exists, push flow onto it. In addition to being slow, Ford-Fulkerson, as it is stated, is not guaranteed to terminate for graphs with non-integral capacities. In fact, it might not even converge to the maximum flow for irrational capacities. However, these problems can be fixed by simply specifying how the algorithm chooses the next path on which to 97 Hsiang, Wei, Liu Chapter 9. More Graph Algorithms push flow. Nonetheless, the Ford-Fulkerson algorithm is formulated beautifully mathematically and such is useful from a math perspective, as we will see with the Max-Flow Min-Cut Theorem. 9.2.2 Max-Flow Min-Cut Theorem On a graph G(V, E), an s-t cut C = (S, T ) splits the partitions V into S and T satisfying s ∈ S and t ∈ T . The cut-set of C is the set of edges {(u, v) ∈ E : u ∈ S, v ∈ T }. The capacity of an s-t cut is given by c(S, T ) = X c(u, v). (u,v)∈S×T A minimum cut is an s-t cut that minimizes c(S, T ). A cut represents a set of edges that, once removed, separates s from t. A minimum cut is therefore a set of edges that does this but minimizes the total capacity of the edges necessary to disconnect s from t. The max-flow min-cut theorem states that the maximum value of any s-t flow is equal to the minimum capacity of any s-t cut. First, the capacity of any cut must be at least the total flow. This is true by contradiction. Any path from s to t has an edge in the cut-set, so therefore any flow from s to t is upper bounded by the capacity of the cut. Therefore, we need only construct one flow and one cut such that the capacity of the cut is equal to the flow. We consider the residual graph Gf produced at the completion of the Ford-Fulkerson augmenting path algorithm.1 Let the set S be all nodes reachable from s and T be V \ S. We wish to show that P C = (S, T ) is a cut satisfying |f | = c(S, T ) = (u,v)∈S×T c(u, v). This is true when the following is satisfied: 1. All edges (u, v) ∈ S × T are fully saturated by the flow. That is, cf (u, v) = 0. 2. All reverse edges (v, u) ∈ T × S have zero flow. That is, f (v, u) = 0, or cf (v, u) = c(v, u). The first condition is true by the way we constructed S and T , as if there existed a (u, v) where cf (u, v) > 0, then v is accessible to s and ought to have been in S. The second condition is true by the way the Ford-Fulkerson algorithm constructed reverse edges. If net flow was sent from v to u, then a reverse edge was constructed from u to v, so again, v is accessible to s, which is a contradiction. Therefore, we have the flow |f | = X c(u, v) − X 0 = c(S, T ), (v,u)∈T ×S (u,v)∈S×T so we constructed a flow and a cut such that the flow |f | is equal to the cut capacity c(S, T ), and we are done. 1 If you’re concerned that the Ford-Fulkerson algorithm will never terminate, there always exists a sequence of paths chosen such that it will. Edmonds-Karp is one example that always terminates. 98 Hsiang, Wei, Liu 9.2.3 Chapter 9. More Graph Algorithms Refinements of Ford-Fulkerson As stated earlier, Ford-Fulkerson is limited by not specifying on which path to push flow. There are many algorithms that resolve this issue in different ways. Edmonds-Karp Edmonds-Karp fixes the problem by simply choosing the augmenting path of shortest unweighted length. This can be done easily using a BFS. Algorithm 11 Edmonds-Karp function ChoosePath(Gf (V, Ef ), s, t ∈ V ) visited(v) denotes v has been added to queue prev(v) denotes vertex preceding v in BFS push s on queue Q visited(s) ← 1 while Q is not empty do u ← top of Q for all neighbors v of u in Gf where visited(v) = 0 do push v on Q visited(v) ← 1 prev(v) ← u pointer curr ← t while curr 6= s do add curr to beginning of path p curr ← prev(curr) add s to beginning of p return p function MaxFlow(G(V, E), s, t ∈ V ) for all (u, v) ∈ V 2 do f (u, v) ← 0 cf (u, v) ← c(u, v) . BFS |f | ← 0 while t can be reached from s do p ← ChoosePath(Gf , s, t) cap ← minm−1 i=1 (cf (vi , vi+1 )) |f | ← |f | + cap AugmentPath(p) return |f | The BFS is clearly O(E). To complete our analysis, we must somehow bound the number of times we need to perform the BFS. To do this, we’ll look at what pushing flow on a path does to our residual graph; in particular, how it affects our BFS traversal tree. Note that each vertex is on some level i in the BFS tree, characterized by the distance from the source s. For example, L0 = {s}, L1 contains all the neighbors of s, L2 contains neighbors of neighbors not in L0 or L1 , and so on. 99 Hsiang, Wei, Liu Chapter 9. More Graph Algorithms We first claim that the level of any vertex in the graph is nondecreasing following an augment on a path p. If the augment saturates an edge, it may remove it from Gf , which cannot decrease the distance of any vertex from s. If the augment creates an edge e = (u, v), that means we sent flow from v to u on the path p. Therefore, if v was originally level i, u must have been level i + 1. The level of u does not change by adding (u, v), and the level of v can either be i or i + 2, depending on whether edge (v, u) was deleted in the process. Either way, the level of all vertices is nondecreasing. Now consider the bottleneck edge e = (u, v) of an augmenting path p, where the level of u is i and the level of v is i + 1. The push operation deletes the edge e, but the level of v must stay at least i + 1. Now for the edge e to reappear in the graph Gf , flow must have been sent on the reverse edge e0 = (v, u) on some augmenting path p0 . But on path p0 , u comes after v, which must be at least level i + 1. Therefore, u must be at least level i. But since the maximum level of a node that is connected to s is V − 1, an edge e can only be chosen as the bottleneck edge V2 times, or O(V ). There are E edges, each of which can be the bottleneck edge for O(V ) different augmenting paths, each of which takes O(E) to process. Therefore, the Edmonds-Karp algorithm runs in O(V E 2 ). Dinic Dinic’s algorithm is another refinement of Ford-Fulkerson. Dinic’s algorithm is similar to EdmondsKarp in that we make use of the length of the shortest path from s to each vertex v. We first define the level graph GL of Gf as the graph containing edges (u, v) only if dist(v) = dist(u) + 1. We then define the blocking flow as a maximum flow f of a level graph GL . Dinic’s algorithm constructs a level graph GL , augments the graph with a blocking flow, and repeats by finding another level graph. This process can happen O(V ) times, as with each blocking flow, dist(t) increases. Each BFS to compute the level graph is O(E), and each blocking flow can be computed in O(V E). Therefore, the run-time of Dinic’s algorithm is O(V 2 E). Using a link-cut tree, the computation of a blocking flow can be improved to O(E log V ), improving the overall run-time to O(V E log V ). In networks with unit capacities, √ each blocking flow can be computed in O(E), and √ the number of total blocking flows is O(min E, V 2/3 ), improving the overall run-time to O(min E, V 2/3 E). 9.2.4 Push-Relabel Unfortunately, Dinic’s algorithm is considerably complex, and even the much-improved bounds of the simpler O(V E 2 ) Edmonds-Karp are admittedly bad. While the push-relabel method for solving the max flow problem does not√have the fastest theoretical bounds, two of its implementations have complexities O(V 3 ) and O(V 2 E) and are among the fastest in practice. Generic Push-Relabel Ford-Fulkerson and its variants all deal with global augmentations of paths from s to t. Push-relabel takes a different perspective, introducing the concept of a preflow and a height to make local optimizations that ultimately result in the maximum flow. 100 Hsiang, Wei, Liu Chapter 9. More Graph Algorithms A preflow maintains the same properties as a flow but modifies the conservation of flow condition. Instead of total flow in equaling total flow out, flow in must be at least, and therefore can exceed, flow out. We denote the difference between flow in and flow out as the excess e(v). f (u, v) ≤ c(u, v) ∀(u, v) ∈ E f (u, v) = −f (v, u) ∀(u, v) ∈ E e(v) = X f (u, v) ≥ 0 ∀v ∈ V \ {s} u∈V e(s) = ∞. The definitions of the residual capacity cf (u, v), edge set Ef , and graph Gf are the same as they were defined before, except with a preflow f instead of a normal flow. We call a vertex v ∈ V \ {s, t} active if e(v) > 0. Therefore, a vertex besides the source or sink is active if more flows into the vertex than flows out. s and t are never active. A preflow with no active vertices is simply a flow, at which point the excess of the sink e(t) represents the value |f | of the flow. We can push flow from a node u to a node v by moving as much of the excess e(u) to v as the capacity of the edge cf (u, v) will allow. function Push(edge (u, v)) δ ← min(e(u), cf (u, v)) . cf (u, v) = c(u, v) − f (u, v) f (u, v) ← f (u, v) + δ f (v, u) ← f (v, y) − δ e(u) ← e(u) − δ e(v) ← e(v) + δ The idea of the push-relabel algorithm is to first push as much preflow as possible through local optimizations in the direction the sink. When a node can no longer push flow to the sink, it pushes the excess back towards the source to turn the preflow into a flow. However, the difficulty here lies in establishing this sense of “direction” from the source to the sink. Remember that we simply push preflow along a single edge in the graph at a time, not along a whole path. Moving flow from the source to the sink along a path that goes from the source to the sink is easy; moving flow from the source to the sink through local pushes without the knowledge of the graph structure as a whole is indeed a much harder problem. To resolve this issue, we introduce a label to each of the nodes. The label h(u) represents the “height” of u. In real life, water flows from higher to lower ground. We want s to represent that high ground and t to represent the low ground. As we push preflow from s to t, vertices along the way represent height values between those of s and t. However, eventually we have to push preflow back to handle both excesses in flow and suboptimal previous pushes, à la Ford-Fulkerson, but this contradicts the concept of height as we can’t flow both downhill and uphill. Therefore, we’ll need to be able to relabel a node, changing the height h(u) to something that allows preflow to flow back towards s. We will relabel h in a systematic way that allows us to direct the preflow through the graph. For this labeling to be useful for us, we’ll need to impose some more constraints that must be satisfied no matter how we change the graph Gf or the height function h. 101 Hsiang, Wei, Liu Chapter 9. More Graph Algorithms h(u) ≤ h(v) + 1∀(u, v) ∈ Ef h(s) = |V | h(t) = 0. What does this mean? For our algorithm, we can push preflow along the edge from u to v only if cf (u, v) > 0 and h(u) > h(v), so h(u) = h(v) + 1. We call such an edge (u, v) ∈ Ef admissible. Furthermore, for all vertices v that can reach t in Ef , h(v) represents the lower bound for the length of any unweighted path from v to t in Gf , and for all vertices that cannot reach t, then h(v) − |V | is a lower bound for the unweighted distance from s to v. t will always represent the lowest node, so h(t) = 0 is a natural constraint. We’ll first set the preflow values of all vertices v that can be immediately reached from s to c(s, v), saturating all the out-edges of s. For any vertex v from which t can be reached, h(v) represents the lower bound of the unweighted distance to t from v in the residual graph. We want h(s) to be a number large enough that will indicate that s has been disconnected to t, as we have already sent as much preflow possible from s in the direction of t by saturating all outgoing edges. Therefore, setting h(s) = |V | is also natural. Since h(s) represents the lower bound of the distance from s to t in Gf , and there are no paths from s to t in the residual graph, |V | is a natural choice, since the longest possible path is |V | − 1. Furthermore, we don’t want any preflow sent back to s from a vertex v unless it is impossible to send any more preflow from v to t. If preflow is pushed from v to s, then h(v) = |V | + 1. If there existed a path v to t such that every edge is admissible, the path must have |V | + 2 vertices. This is true because for any two consecutive vertices vi , vi+1 in the path, h(vi ) = h(vi+1 ) + 1, but no path can have |V | + 2 distinct vertices. This leads to the fact that the only nodes that can possibly continue to contribute to the final flow are active vertices v for which h(v) < |V |. A node with height at least |V | does not have a valid path to t, and a node that is not active doesn’t have any excess flow to push. Now that I’ve explained the general idea behind the labeling constraints, it’s time to actually describe what our relabeling process is. At first, the labels of all vertices besides the source start at 0. We only relabel a node v if it is active (therefore, it has excess flow it needs to push; e(u) > 0) but has no admissible out-edges in Gf (so it has no adjacent vertex on which it can push that excess flow). If a node has no admissible out-edges in Gf , every neighbor of u has a height label at least equal to h(u). When we relabel a node, we always then increase the value of h(u) to the least value where it can push flow onto another node. function Relabel(vertex u) . (u, v) ∈ Ef ⇐⇒ cf (u, v) = c(u, v) − f (u, v) > 0 h(u) ← minv|(u,v)∈Ef (h(v)) + 1 Since we take the minimum height of all neighbors in the graph, we first try adjusting the height of u so that we can push flow from u to its neighbors that can possibly still reach t; that is, neighbors v satisfying h(v) < |V |. Once we try all these neighbors, we then increase the height of u to begin to push flow back towards s. We can always find such an edge, as any preflow pushed onto u must have also incremented the reverse edge from u back towards s. Note that neither pushing on a admissible edge nor relabeling a vertex with no admissible out-edges changes the fact that h remains a valid labeling function. 102 Hsiang, Wei, Liu Chapter 9. More Graph Algorithms The generic push-relabel algorithm simply pushes and relabels vertices until there are no active vertices and the preflow becomes a flow. This algorithm works because throughout the process, h remained a valid height function, and at the end, the preflow was converted into a flow. Since h(s) = |V | and h(t) = 0, there is no augmenting path from s to t, so our flow is maximal. Algorithm 12 Push-Relabel (Generic) function Push(edge (u, v)) if e(u) > 0 and h(u) = h(v) + 1 then . push condition δ ← min(e(u), cf (u, v)) . cf (u, v) = c(u, v) − f (u, v) f (u, v) ← f (u, v) + δ f (v, u) ← f (v, y) − δ e(u) ← e(u) − δ e(v) ← e(v) + δ function Relabel(vertex u) if u 6= s, t and e(u) > 0 and h(u) ≤ h(v)∀v|(u, v) ∈ Ef then . relabel condition h(u) ← minv|(u,v)∈Ef (h(v)) + 1 . (u, v) ∈ Ef ⇐⇒ cf (u, v) = c(u, v) − f (u, v) > 0 function MaxFlow(G(V, E), s, t ∈ V ) for all v ∈ V \ {s} do . initialize excess e(v) ← 0 e(s) ← ∞ for all (u, v) ∈ V 2 do . initialize preflow f (u, v) ← 0 for all neighbors v 6= s of s do . saturate edges to all neighbors of s f (s, v) ← c(s, v) f (v, s) ← −c(s, v) e(v) ← c(s, v) for all v ∈ V \ {s} do . preflow now has valid height function; initialize height h(v) ← 0 h(s) ← |V | while we can still Push or Relabel do Push or Relabel return e(t) . e(t) = |f | We can argue that this algorithm runs in O(V 2 E), which is already an improvement from Edmonds-Karp. However, just as Ford-Fulkerson could be sped up by specifying which augmenting paths to choose, we can do the same with the push-relabel algorithm, speeding it up by specifying a systematic method to choose an edge to push or a vertex to relabel. Discharge We first describe an auxiliary operation. For each vertex u, we’ll need a way to visit in- and out-neighbors of u in a static cyclic order. This is easy with just a pointer; for vertex u, we’ll call that pointer curr(u). When the pointer passes through every element in the list of neighbors, we’ll just reset it back to the first element. 103 Hsiang, Wei, Liu Chapter 9. More Graph Algorithms function Discharge(vertex u) while e(u) > 0 do . perform an operation as long as u is active if curr(u) is at end of list of neighbors then Relabel(u) reset curr(u) else if (u, curr(u)) is an admissible edge then Push((u, curr(u))) else move curr(u) to next neighbor of u FIFO Selection FIFO selection simply maintains a list of active vertices in a FIFO queue. We pop off the first vertex in the queue and discharge it, adding any newly-activated vertices to the end of the queue. This runs in O(V 3 ). Highest Label Selection √ Highest label selection discharges the active vertex with the greatest height. This runs in O(V 2 E). Improvements with Heuristics Heuristics are meant to help relabel vertices in a smarter way. Bad relabelings are the slowest part of the algorithm, and improving the process can speed up max flow. The gap heuristic takes advantage of “gaps” in the height function. Since a path of admissible edges consists of vertices whose heights decrease by exactly 1, the presence of a gap in height precludes the possibility of such a path. If there exists a value h0 such that no vertex v exists such that h(v) = h0 , then for every vertex v satisfying h0 < h(v) < |V |, v has been disconnected from t, so we can immediately relabel h(v) = |V | + 1. The global relabeling heuristic performs a backwards BFS from t every now and then to compute the heights of the vertices in the graph exactly. Some dude on Codeforces2 didn’t have much luck improving performance with the global relabeling heuristic. I’d suggest sticking to the gap heuristic only. 9.2.5 Extensions 9.2.6 Bipartite Matchings The bipartite matching problem is perhaps the most well-known problem solvable by flows. 2 http://codeforces.com/blog/entry/14378 104 Chapter 10 Math Algorithms here exhibit a different flavor than the graph theory, string, or geometry algorithms from earlier. This chapter is placed towards the end because material here doesn’t really fit in any other section or the USACO canon, not because this material is particularly difficult. 10.1 Number Theory This section will contain much information about primes in many different ways. Indeed, prime numbers are the building blocks of number theory! 10.1.1 Random Prime Numbers Bounds The point of this section is mostly to make the complexity analysis of the later sections easier. Therefore, I will list some useful theorems and bounds without proof. The proofs can be found in any standard number theory textbook or online. They are extremely insignificant in the context of the goals of this section. Prime Number Theorem: The number of prime numbers less than n is O( logn n ) for any positive integer n. Corollary: The n-th prime number, pn , is O(n log n). Prime Number Harmonic Sum: 10.1.2 1 p≤n p P = O(log log n). Prime Number Testing Let’s say if you want to test is a number n is prime. The most obvious way is to test all integers from 2 to n − 1, and see if they divide n. This takes O(n) time. We can do better though! Note that √ if n has a nontrivial factor, then it has one that is less than n. Indeed, if n = pq, (as a nontrivial √ √ 2 factorization) if both p, q > n, then pq > n = n, a contradiction. So we only need to check √ √ whether n has factors between 2 and n. This takes O( n) time. If you have precomputed a list of √ prime numbers (up to say n), ten you only need to check whether n has any prime factors in the √ √ n same 2 to n. This would give runtime O( log n ) from the Prime Number Theorem in section 10.1.1, a small but nontrivial improvement. 105 Hsiang, Wei, Liu 10.1.3 Chapter 10. Math Sieve of Eratosthenes If you want to compute all primes in the range 1 to n. Runs in O(n log log n). In other words, super fast. 10.1.4 Prime Factorization How can we prime factorize a number? That is, given a positive integer n, how can we write it in the form p1 · p2 · · · · · pk , where the pi are all primes? Form example, 30 = 2 · 3 · 5 and 24 = 2 · 2 · 2 · 3. The fact that every number can be uniquely factorized is called the Fundamental Theorem of Arithmetic. Let’s now figure out how to prime factorize every number efficiently. Using our intuition from above, √ we should expect that checking up to around n suffices. This is indeed true. The code below demonstrates: 10.1.5 GCD and the Euclidean Algorithm I’ll start once again with a theorem in number theory: if positive integers a, b satisfy gcd(a, b) = 1, then there exist integers x, y such that ax + by = 1. This is called Bezout’s Lemma. Similarly, if gcd(a, b) = g, then there are integers x, y such that ax + by = g. 10.1.6 Fermat’s Little Theorem Fermat’s Little Theorem states that ap ≡ a (mod p) for any positive integer a. Equivalently, we can say that ap−1 ≡ 1 (mod p). 10.1.7 Modular Inverses If p is a prime, and x satisfies gcd(x, p) = 1, then there exists a positive integer k such that kx ≡ 1 (mod p). k is called the modular inverse of x (mod p), often just denoted as 1/x or x−1 . Indeed, 1/x · x = x−1 · x = 1, so this notation is natural. The cool thing about modular inverses is that you can see them as division modulo p. Since x is nonzero (mod p), it makes sense that you should be able to divide by x. This is the main application of modular inverses. But how to find one quickly? There are two common algorithms, both of which run in O(log p) time. You can do this by running the Euclidean Algorithm described above: find positive integers j, k such that pj + kx = 1. Then obviously kx ≡ 1 (mod p). The other way is to notice that xp−1 ≡ 1 (mod p) by Fermat’s Little Theorem, so xp−2 ≡ x−1 (mod p). Now compute the left hand side of the previous expression using binary exponentiation. m k Exercises related: Do O(n log p) precomputation to compute any binomial coefficient of the form (mod p) where 0 ≤ m, k ≤ n in O(1) time. 10.2 Combinatorial Games https://activities.tjhsst.edu/sct/lectures/1314/impartial_games_12_06_13.pdf 106 Hsiang, Wei, Liu Chapter 10. Math 10.3 Karatsuba 10.4 Matrices 10.5 Fast Fourier Transform Introduction The Fast Fourier Transform (FFT) is a technique used to multiply two polynomials of degree n in O(n · log n) time. At a high level, what this algorithm is doing is something called polynomial interpolation. This refers to the fact that if we know the value of a polynomial P of degree n at n + 1 points, then we can uniquely determine the polynomial P. The proof of this statement is simple, and involves the Lagrange Interpolation Formula for one direction and a simple root counting argument to prove uniqueness. I won’t go into detail here because the proof isn’t important, but it is useful to know some motivation behind the algorithm. Algorithm Outline Let’s say we want to multiply 2 polynomials A(x), B(x), both of degree n. Let C(x) = A(x)B(x). The algorithm will proceed in 3 steps. Choose an integer m > 2n, and choose m numbers x0 , x1 , . . . , xm−1 . I’ll clarify what to choose m and the xi as later. Just keep in mind that we can choose these values to be anything we want. 1. Evaluate A(x0 ), A(x1 ), . . . , A(xm−1 ). Similarly, evaluate B(x0 ), B(x1 ), . . . , B(xm−1 ). 2. Evaluate C(xi ) = A(xi )B(xi ) for 0 ≤ i ≤ m − 1. 3. Interpolate the coefficients of C(x) given the values C(x0 ), C(x1 ), . . . , C(xm−1 ). The last step explains why we need m > 2n. The degree of C(x) is 2n, so we need at least 2n + 1 points to determine C(x) uniquely. You should be skeptical that the above approach does any better than O(n2 ). In particular, step 1 seems like it should take O(n2 ). It turns out that if we choose the values x0 , x1 , . . . , xm−1 properly, then we can do much better. Let me now describe what to choose m to be, and what to choose x0 , x1 , . . . , xm−1 to be. Roots of Unity Before telling you precisely, here’s some food for thought. Let’s take then example polynomial A(x) = 1 + 2x + 3x2 + 4x3 + 5x4 + 6x5 + 7x6 + 8x7 . Let’s split this into 2 groups: the even degree coefficients and odd degree coefficients. Let’s call these two groups Aeven and Aodd . Define Aeven (x) = 1 + 3x + 5x2 + 7x3 , Aodd (x) = 2 + 4x + 6x2 + 8x7 . Then clearly A(x) = Aeven (x2 ) + x · Aodd (x2 ) 107 Hsiang, Wei, Liu Chapter 10. Math Notice that the x2 in the formula makes it extremely easy to compute A(−x) given A(x). It’s only one sign flip off! Therefore, it would be cool if our xi above were symmetric with respect to 0, i.e. if we want to compute x, we also should want to compute −x. An example set like this is {1, 2, 3, 4, −1, −2, −3, −4}. So if we wanted to compute A(x) at these values, we would need to compute the values of Aeven , Aodd at their squares, that is {1, 4, 9, 16}. But this set is no longer symmetric! So this is not what we want exactly, since that means that we can’t just recursively compute Aeven and Aodd . So let’s say that the set we want to compute Aeven , Aodd on is something like {1, 4, −1, −4}. Then the values that we get for A(x) are {1, −1, 2, −2, i, −i, 2i, −2i}. Complex numbers! This is closer to what we want, but still not precisely. The selection of the xi explained below makes everything work out perfectly. So what should we choose m and the xi to be? I’ll tell you now and then explain why this selection works so well: choose m to be a power of 2 that is larger than 2n, and choose x0 , x1 , . . . , xm−1 to be the m-th roots of unity. The m-th of unity roots are complex numbers that satisfy the equation 2kπi m x = 1. They are of the form cos m + i · sin 2kπi for any integer k from 0 to m − 1. m 0 0 m 0 Let ω be a primitive root of unity, i.e. the smallest m such that ω = 1 is m = m. We can 2πi without loss of generality set ω = cos 2πi m + i · sin m . One can easily check the the remaining roots of unity are ω 2 , ω 3 , . . . , ω m . From now on, let’s just set xi = ω i for all 0 ≤ i ≤ m − 1. Note that x0 = 1. Also set m = 2k > 2n. Now I can proceed to describing why this algorithm works. Algorithm Specifics: Step 1 In this section I’ll implement (using pseudocode) a function vector fft(vector A, k, ω). This means that this will return a vector k k of length 2k containing the values A(ω 0 ), A(ω 1 ), A(ω 2 ), . . . , A(ω 2 −1 ). Remember that ω 2 = 1. The vector A stores the coefficients of A(x). The xi coefficient of A would be stored as A[i]. Here’s an implementation. The brackets {} will be shorthand for containing a vector. Algorithm 13 Fast Fourier Transform k . ω2 = 1 function FFT(vector A, k, ω) A.resize(2k ) vector f, feven , fodd f .resize(2k ) if k = 0 then return {A[0]} Aeven = {A[0], A[2], . . . , A[2k − 2]} Aodd = {A[1], A[3], . . . , A[2k − 1]} feven = FFT(Aeven , k − 1, ω 2 ) fodd = FFT(Aodd , k − 1, ω 2 ) for all i = 0, i < 2k−1 , i + + do f [i] = feven [i] + ω i · fodd [i] f [i + 2k−1 ] = feven [i] − ω i · fodd [i] return f . a vector of length 1 108 Hsiang, Wei, Liu Chapter 10. Math So why does this algorithm work? Note that the algorithm proceeds recursively, calling itself twice (once for even, once for odd). Aeven (x) is the polynomial A[0] + A[2]x + A[4]x2 + · · · + k−1 A[2k − 2]x2 −1 . Therefore, what the recursive call is doing is computing the values of Aeven at k the values x = ω 0 , ω 2 , ω 4 , . . . , ω 2 −2 . This is equivalent to computing the values of the polynomial k k−1 Beven (x) = A[0] + A[2]x2 + A[4]x4 + · · · + A[2k − 2]x2 −2 at the values x = ω 0 , ω 1 , ω 2 , . . . , ω 2 −1 . k−1 The recursive call for Aodd (x) = A[1] + A[3]x + A[5]x2 + · · · + A[2k − 1]x2 −1 behaves in a similar k way. Similarly, define Bodd (x) = A[1] + A[3]x2 + A[5]x4 + · · · + A[2k − 1]x2 −2 . The key is to note that A(x) = Beven (x)+x·Bodd (x). Since we know the values of Beven (x), Bodd (x) k−1 for x = ω 0 , ω 1 , . . . , ω 2 −1 , we also can compute the values of A(x) for these values of x. But k−1 what about the remaining values? This requires the observation that ω i+2 = −ω i . Since k−1 i i i+2 Beven (x), Bodd (x) only have even powers, Beven (ω ) = Beven (−ω ) = Beven (ω ). The same equality also holds for Bodd (x). Using this observation along with the equation A(x) = Beven (x) + x · Bodd (x), we can see now why the two equations in the for loop of the code above hold. And that’s how we do Step 1! Let’s analyze the runtime of this function FFT. Let T (2k ) denote the time needed to run FFT on an array of length 2k . Then T (2k ) = 2T (2k−1 ) + O(2k ) =⇒ T (2k ) = k · 2k , by the Master Theorem. Since 2k is O(n) by the above discussion, this steps runs in O(n · log n) time. Algorithm Specifics: Step 2 I’ll give you a rest after a hard theoretical previous section. You do this step by looping through the values A(x0 ), A(x1 ), . . . , A(x2k −1 ), B(x0 ), B(x1 ), . . . , B(x2k −1 ) and directly multiplying C(xi ) = A(xi )B(xi ). This step runs in O(n) time. Algorithm Specifics: Step 3 This may seem like something completely new, but actually most of the work is already done. I’ll explain. In fact I claim that writing the code C = fft(C, k, ω −1 ), and then dividing all elements of C by 2k finishes. Step 3 is very easy once you have Step 1! To see this, let’s take a look at how step 1 works. We’ll consider the coefficients of A as a vector (not a C++ vector, but an actual math vector – matrices!). 0 A(ω ) 0 ω A(ω 1 ) ω 0 A(ω 2 ) ω 0 A(ω 3 ) = ω 0 . .. . . . k A(ω 2 −2 ) ω 0 k A(ω 2 −1 ) ω0 ω0 ω0 ··· ω0 2k −2 ω1 ω2 ω3 ··· ω ω2 ω4 ω6 ··· ω 2(2 ω3 .. . ω6 .. . ω9 .. . ··· .. . ω 3(2 −2) .. . k −2 ω 2(2 k −1 ω 2(2 ω2 ω0 ω2 k −2) ω 3(2 k −1) ω 3(2 k −2) k k −2) ··· ω (2 k −1) ··· ω (2 109 k −2)(2k −2) k −2)(2k −1) ω0 A[0] ω A[1] k −1) 2(2 ω A[2] k ω 3(2 −1) A[3] .. .. . . k k (2 −1)(2 −2) k ω A[2 − 2] k k 2k −1 ω (2 −1)(2 −1) A[2k − 1] Hsiang, Wei, Liu Chapter 10. Math Now suppose we perform the FFT with ω −1 . This is equivalent by multiplying by a similar matrix. 110 Hsiang, Wei, Liu ω0 ω 0 0 ω ω 0 . . . ω 0 Chapter 10. Math ω0 ω0 ω0 ω −1 ω −2 ω −2 ω −3 .. . ω 0 0 ω 0 = ω . . . ω 0 ··· k ω −(2 −2) ω −4 ω −6 ··· ω −2(2 ω −6 .. . ω −9 .. . ··· .. . ω −3(2 .. . ω −2(2 k −1) ω −2(2 ω 0 ω −(2 ω0 ω −3 k −2) ω −(2 ··· ω0 ω0 k −2) ω −3(2 k −1) ω −3(2 ω0 ω −1 ω −(2 k −2)(2k −2) k −1) ··· ω −(2 k −2)(2k −1) ··· ω −3 ··· ω −(2k −2) −2(2k −2) ··· ω ω −3 .. . ω −6 .. . ω −9 .. . ··· .. . ω −3(2 .. . ω 0 ω −(2 k −1) ω −2(2 k −2) ω −3(2 k −1) ω −3(2 0 0 ··· 0 2k 0 0 ··· 0 0 2k 0 ··· 0 0 .. . 0 .. . 2k · · · .. . . . . 0 .. . 0 0 0 ··· 2k 0 0 0 ··· 0 k −2) ··· ω −(2 k −1) ··· ω −(2 0 k −2) k −2)(2k −2) A[0] 0 A[1] 0 A[2] 0 A[3] .. .. . . k 0 A[2 − 2] 0 2k A[2k − 1] A[0] A[1] A[2] = 2k A[3] . .. . A[2k − 2] A[2k − 1] 111 k −2)(2k −1) A(ω 0 ) A(ω 1 ) k A(ω 2 ) ω −2(2 −1) k −1) 3 −3(2 ω A(ω ) .. .. . . k k A(ω 2k −2 ) ω −(2 −1)(2 −2) k k k k ω −(2 −1) ω −(2 −1)(2 −1) ω0 ω −6 ω −2(2 0 ··· ω −4 k −2) 0 0 = 0 . . . 0 k −2) ω −2 ω −(2 2k k −2) k −2) ω0 ω −2 ω0 ω0 A(ω 2 ω0 ω 0 ω 0 k ω −2(2 −1) ω k 0 ω −3(2 −1) ω .. .. . . k −1)(2k −2) 0 −(2 ω ω k k −(2k −1) ω −(2 −1)(2 −1) −1 ) ω0 ω0 ω0 ··· ω0 2k −2 ω1 ω2 ω3 ··· ω ω2 ω4 ω6 ··· ω 2(2 ω3 .. . ω6 .. . ω9 .. . ··· .. . ω 3(2 −2) .. . k −2 ω 2(2 k −1 ω 2(2 ω2 ω0 ω2 k −2) ω 3(2 k −1) ω 3(2 k −2) k k −2) ··· ω (2 k −2)(2k −2) k −1) ··· ω (2 k −2)(2k −1) ω0 A[0] A[1] ω k A[2] ω 2(2 −1) k −1) 3(2 ω A[3] .. .. . . k −1)(2k −2) k (2 ω A[2 − 2] k k 2k −1 ω (2 −1)(2 −1) A[2k − 1] Hsiang, Wei, Liu Chapter 10. Math It follows that the inverse FFT is simply the same function, but run with ω −1 instead of ω, and dividing out m = 2k . A Brief Note on Number Theory There is a slight variation on FTT that avoids using complex numbers completely. Complex numbers are often bad at precision and run slowly. There is a way to make FFT work over only the integers, which is pretty cool. Here’s a brief description how. We work (mod p) for a suitable prime p. The reason we need p to be prime is so that division works as we want. As explained in the above section on number theory, every nonzero number modulo p has a unique inverse, so division works as expected. There are also integers g such that g 0 , g 1 , . . . , g p−2 run through all nonzero numbers (mod p). These are called primitive roots. This means that modulo p behaves a whole lot like the roots of unity that we want. More specifically, we want an integer ω such that the smallest integer m that satisfies ω m = 1 is m = 2k , for some power of 2. So when can we find such an ω? By the discussion of primitive roots, this can happen precisely p−1 when 2k |p − 1. Then all we do is find a primitive root g, and take ω = g 2k . Finding primitive roots is fast because math proves that there is always a small primitive root. So everything works just as you expect! The same exact code works, just put a (mod p) at the end of every line and you’re set. 112 Chapter 11 Nonsense Have fun. 11.1 Segment Tree Extensions 11.1.1 Fractional Cascading 11.1.2 Persistence 11.1.3 Higher Dimensions 11.2 DP Optimizations 11.3 Top Tree Tree representation of a graph. Scary. 11.4 Link-Cut Cactus A cactus is a graph such that any two distinct cycles in the graph share at most one vertex. Graphs that are not trees are somehow hard to characterize; a cactus is a more general kind of graph that still has some nice properties. In particular, certain tree algorithms still run in polynomial time when modified on a cactus where no such generalization to all graphs exists. Cacti are scary. 113 Chapter 12 Problems Problems, to be worked through as you progress through the text. 12.1 Bronze USACO bronze is quite ad hoc. Knowing basic data structures in the standard library helps. 12.2 Silver USACO silver tends to have the most standard problems. Silver tests knowledge of basic algorithms and coding ideas. Silver questions also sometimes require knowledge of the language, like familiarity with standard library data structures. 12.2.1 Complete Search 12.2.2 Greedy 12.2.3 Standard Dynamic Programming 1. (USACO Training) There is a long list of stalls, some of which need to be covered with boards. You can use up to N (1 ≤ N ≤ 50) boards, each of which may cover any number of consecutive stalls. Cover all the necessary stalls, while covering as few total stalls as possible. 2. (IOI 1996) You are given a three-valued (1, 2, or 3) sequence of length up to 1000. Find a minimum set of exchanges to put the sequence in sorted order. 3. (Samir Khuller) We are given N jobs, each of which requires one unit of time to complete. The ith job opens at some time ti and must be completed by some deadline di , where ti , di ∈ Z. Given that only one job can be completed at a time, determine if all N can be completed by their deadlines. 115 Hsiang, Wei, Liu 12.2.4 Chapter 12. Problems Standard Graph Theory Flood Fill Shortest Path 1. (USACO Training Pages, butter) Farmer John owns a collection of pastures with weighted edges between some pairs of locations. Each pasture is inhabited by a cow, and the cows wish to all congregate at one of the pastures. Find the pasture at which the cows should meet in order to minimize combined travel distance. 2. (USACO February 2012, relocate) FJ is moving! He is trying to find the best place to build a new farm so as to minimize his daily travel time. The region to which FJ plans to move has N towns (1 ≤ N ≤ 10, 000). There are M bidirectional roads (1 ≤ M ≤ 50, 000) connecting certain pairs of towns. All towns are reachable from each-other via some combination of roads. FJ needs your help selecting the best town as the home for his new farm. There are markets in K of the towns (1 ≤ K ≤ 5) that FJ wants to visit every day. In particular, every day he plans to leave his new farm, visit the K towns with markets, and then return to his farm. FJ can visit the markets in any order he wishes. When selecting a town in which to build his new farm, FJ wants to choose only from the N − K towns that do not have markets, since housing prices are lower in those towns. Please help FJ compute the minimum distance he will need to travel during his daily schedule, if he builds his farm in an optimal location and chooses his travel schedule to the markets as smartly as possible. 3. (USACO December 2012, mroute) Farmer John’s farm has an outdated network of M pipes (1 ≤ M ≤ 500) for pumping milk from the barn to his milk storage tank. He wants to remove and update most of these over the next year, but he wants to leave exactly one path worth of pipes intact, so that he can still pump milk from the barn to the storage tank. The pipe network is described by N junction points (1 ≤ N ≤ 500), each of which can serve as the endpoint of a set of pipes. Junction point 1 is the barn, and junction point N is the storage tank. Each of the M bi-directional pipes runs between a pair of junction points, and has an associated latency (the amount of time it takes milk to reach one end of the pipe from the other) and capacity (the amount of milk per unit time that can be pumped through the pipe in steady state). Multiple pipes can connect between the same pair of junction points. For a path of pipes connecting from the barn to the tank, the latency of the path is the sum of the latencies of the pipes along the path, and the capacity of the path is the minimum of the capacities of the pipes along the path (since this is the “bottleneck” constraining the overall rate at which milk can be pumped through the path). If FJ wants to send a total of X units of milk through a path of pipes with latency L and capacity C, the time this takes is therefore L + X C. Given the structure of FJ’s pipe network, please help him select a single path from the barn to the storage tank that will allow him to pump X units of milk in a minimum amount of total time. 116 Hsiang, Wei, Liu Chapter 12. Problems 4. (IOI 1999, Traffic Lights) In the city of Dingilville the traffic is arranged in an unusual way. There are junctions and roads connecting the junctions. There is at most one road between any two different junctions. There is no road connecting a junction to itself. Travel time for a road is the same for both directions. At every junction there is a single traffic light that is either blue or purple at any moment. The color of each light alternates periodically: blue for certain duration and then purple for another duration. Traffic is permitted to travel down the road between any two junctions, if and only if the lights at both junctions are the same color at the moment of departing from one junction for the other. If a vehicle arrives at a junction just at the moment the lights switch it must consider the new colors of lights. Vehicles are allowed to wait at the junctions. You are given the city map which shows • the travel times for all roads (integers), • the durations of the two colors at each junction (integers) • the initial color of the light and the remaining time (integer) for this color to change at each junction. Your task is to find a path which takes the minimum time from a given source junction to a given destination junction for a vehicle when the traffic starts. In case more than one such path exists you are required to report only one of them. Minimal Spanning Tree 1. (USACO March 2014, irrigation) Due to a lack of rain, Farmer John wants to build an irrigation system to send water between his N fields (1 ≤ N ≤ 2000). Each field i is described by a distinct point (xi , yi ) in the 2D plane, with 0 <= xi, yi <= 1000. The cost of building a water pipe between two fields i and j is equal to the squared Euclidean distance between them: (xi − xj )2 + (yi − yj )2 FJ would like to build a minimum-cost system of pipes so that all of his fields are linked together – so that water in any field can follow a sequence of pipes to reach any other field. Unfortunately, the contractor who is helping FJ install his irrigation system refuses to install any pipe unless its cost (squared Euclidean length) is at least C (1 ≤ C ≤ 1, 000, 000). Please help FJ compute the minimum amount he will need pay to connect all his fields with a network of pipes. 2. (USACO February 2015, superbull) Bessie and her friends are playing hoofball in the annual Superbull championship, and Farmer John is in charge of making the tournament as exciting as possible. A total of N (1 ≤ N ≤ 2000) teams are playing in the Superbull. Each team is assigned a distinct integer team ID in the range [1, 230 − 1] to distinguish it from the other teams. The Superbull is an elimination tournament – after every game, Farmer John chooses which team to eliminate from the Superbull, and the eliminated team can no longer play in any more games. The Superbull ends when only one team remains. Farmer John notices a very unusual property about the scores in matches! In any game, the combined score of the two teams always ends up being the bitwise exclusive OR (XOR) of 117 Hsiang, Wei, Liu Chapter 12. Problems the two team IDs. For example, if teams 12 and 20 were to play, then 24 points would be scored in that game, since 01100XOR10100 = 11000. Farmer John believes that the more points are scored in a game, the more exciting the game is. Because of this, he wants to choose a series of games to be played such that the total number of points scored in the Superbull is maximized. Please help Farmer John organize the matches. 3. (SPOJ, INVENT) Given tree with N (1 ≤ N ≤ 15, 000) vertices, find the minimum possible weight of a complete graph (a graph where every pair of vertices is connected) such that the given tree is its unique minimum spanning tree. Union-Find 1. (USACO February 2013, tractor) One of Farmer John’s fields is particularly hilly, and he wants to purchase a new tractor to drive around on it. The field is described by an N × N grid of non-negative integer elevations (1 ≤ N ≤ 500). A tractor capable of moving from one grid cell to an adjacent cell (one step north, east, south, or west) of height difference D costs exactly D units of money. FJ would like to pay enough for his tractor so that, starting from some grid cell in his field, he can successfully drive the tractor around to visit at least half the grid cells in the field (if the number of total cells in the field is odd, he wants to visit at least half the cells rounded up). Please help him compute the minimum cost necessary for buying a tractor capable of this task. 2. (CF 266E) There are n (1 ≤ n ≤ 105 ) employees working in company “X” (let’s number them from 1 to n for convenience). Initially the employees didn’t have any relationships among each other. On each of m (1 ≤ m ≤ 105 ) next days one of the following events took place: • either employee y became the boss of employee x (at that, employee x didn’t have a boss before); • or employee x gets a packet of documents and signs them; then he gives the packet to his boss. The boss signs the documents and gives them to his boss and so on (the last person to sign the documents sends them to the archive); • or comes a request of type “determine whether employee x signs certain documents”. Your task is to write a program that will, given the events, answer the queries of the described type. At that, it is guaranteed that throughout the whole working time the company didn’t have cyclic dependencies. Euler Tour 1. (USACO Training, Airplane Hopping) Given a collection of cities, along with the flights between those cities, determine if there is a sequence of flights such that you take every flight exactly once, and end up at the place you started. (In other words, find an Eulerian circuit on a directed graph.) 118 Hsiang, Wei, Liu Chapter 12. Problems 2. (USACO Training, Cows on Parade) Farmer John has two types of cows: black Angus and white Jerseys. While marching 19 of their cows to market the other day, John’s wife Farmeress Joanne, noticed that all 16 possibilities of four successive black and white cows (e.g., bbbb, bbbw, bbwb, bbww, . . . , wwww) were present. Of course, some of the combinations overlapped others. Given N (2 ≤ N ≤ 15), find the minimum length sequence of cows such that every combination of N successive black and white cows occurs in that sequence. (The answer is not hard to guess, but use Eulerian circuits to prove that it is correct.) 12.2.5 Easy Computational Geometry 12.3 Gold USACO gold problems generally fall into two families; the first tests knowledge of more complex data structures not in the standard library, and the second tests cleverness with more nonstandard greedy or dynamic programming strategies. 12.3.1 More Dynamic Programming 12.3.2 Binary Search 1. (USACO March 2014, sabotage) Farmer John’s arch-nemesis, Farmer Paul, has decided to sabotage Farmer John’s milking equipment! The milking equipment consists of a row of N (3 ≤ N ≤ 100, 000) milking machines, where the ith machine produces Mi units of milk (1 ≤ Mi ≤ 10, 000). Farmer Paul plans to disconnect a contiguous block of these machines – from the ith machine up to the jth machine (2 ≤ i ≤ j ≤ N − 1); note that Farmer Paul does not want to disconnect either the first or the last machine, since this will make his plot too easy to discover. Farmer Paul’s goal is to minimize the average milk production of the remaining machines. Farmer Paul plans to remove at least 1 cow, even if it would be better for him to avoid sabotage entirely. Fortunately, Farmer John has learned of Farmer Paul’s evil plot, and he is wondering how bad his milk production will suffer if the plot succeeds. Please help Farmer John figure out the minimum average milk production of the remaining machines if Farmer Paul does succeed. 2. (Matthew Savage) Ashley’s journey through Unova is just beginning, and she has just picked her first Pokémon! Unfortunately, not knowing much about them, she picked a Snivy, and a particularly stubborn and unfriendly one at that. Being Ashley, she decides to try to win over the Snivy in the only way she knows how – baked goods. Ashley knows r (0 ≤ r ≤ 1000) recipes for Poképuffs. Each recipe ri has a deliciousness rating di (0 ≤ di ≤ 1000) and requires some combination of the I (0 ≤ I ≤ 1000) available ingredients. (More specifically, the recipe ri uses the quantity Iij (0 ≤ Iij ≤ 1000) of ingredient Ij .) 119 Hsiang, Wei, Liu Chapter 12. Problems Ashley has some amount of each ingredient Ij on hand Aj (0 ≤ Aj ≤ 109 ) and can buy more from the nearby store for a price of cj (1 ≤ cj ≤ 109 ) dollars per unit using the M (0 ≤ M ≤ 1012 ) dollars she currently has. Of course, Ashley also has limited supplies and therefore can only produce Poképuffs from a single recipe. However, she can make as many as she wants, in integer increments. We define “total deliciousness” (D) to be the sum of the deliciousnesses of the individual Poképuffs that Ashley has baked. Ashley wants to have the best chance possible with Snivy, and therefore would like to know what is the maximum possible deliciousness (max(D)) that she can produce? Note: there is a “just do it” solution that is faster than the binary search by a log factor. It is also much more annoying to code; so annoying that I was unable to debug my “just do it” solution in the actual contest environment. I included this problem as an exercise to demonstrate how easy binary searching on the answer can be. 3. (CF 287B) Vova, the Ultimate Thule new shaman, wants to build a pipeline. As there are exactly n (1 ≤ n ≤ 1018 ) houses in Ultimate Thule, Vova wants the city to have exactly n pipes, each such pipe should be connected to the water supply. A pipe can be connected to the water supply if there’s water flowing out of it. Initially Vova has only one pipe with flowing water. Besides, Vova has several splitters. A splitter is a construction that consists of one input (it can be connected to a water pipe) and x output pipes. When a splitter is connected to a water pipe, water flows from each output pipe. You can assume that the output pipes are ordinary pipes. For example, you can connect water supply to such pipe if there’s water flowing out from it. At most one splitter can be connected to any water pipe. Vova has one splitter of each kind: with 2, 3, 4, . . . , k (2 ≤ k ≤ 109 ) outputs. Help Vova use the minimum number of splitters to build the required pipeline or otherwise state that it’s impossible. Vova needs the pipeline to have exactly n pipes with flowing out water. Note that some of those pipes can be the output pipes of the splitters. 4. (IOI 2009) Mecho the bear has found a little treasure - the bees’ secret honeypot, which is full of honey! He was happily eating his newfound treasure until suddenly one bee saw him and sounded the bee alarm. He knows that at this very moment hordes of bees will emerge from their hives and start spreading around trying to catch him. He knows he has to leave the honeypot and go home quickly, but the honey is so sweet that Mecho doesn’t want to leave too soon. Help Mecho determine the latest possible moment when he can leave. Mecho’s forest is represented by a square grid of N by N (1 ≤ N ≤ 800) unit cells, whose sides are parallel to the north-south and east-west directions. Each cell is occupied by a tree, by a patch of grass, by a hive or by Mecho’s home. Two cells are considered adjacent if one of them is immediately to the north, south, east or west of the other (but not on a diagonal). Mecho is a clumsy bear, so every time he makes a step, it has to be to an adjacent cell. Mecho can only walk on grass and cannot go through trees or hives, and he can make at most S (1 ≤ S ≤ 1000) steps per minute. At the moment when the bee alarm is sounded, Mecho is in the grassy cell containing the honeypot, and the bees are in every cell containing a hive (there 120 Hsiang, Wei, Liu Chapter 12. Problems may be more than one hive in the forest). During each minute from this time onwards, the following events happen in the following order: • If Mecho is still eating honey, he decides whether to keep eating or to leave. If he continues eating, he does not move for the whole minute. Otherwise, he leaves immediately and takes up to S steps through the forest as described above. Mecho cannot take any of the honey with him, so once he has moved he cannot eat honey again. • After Mecho is done eating or moving for the whole minute, the bees spread one unit further across the grid, moving only into the grassy cells. Specifically, the swarm of bees spreads into every grassy cell that is adjacent to any cell already containing bees. Furthermore, once a cell contains bees it will always contain bees (that is, the swarm does not move, but it grows). In other words, the bees spread as follows: When the bee alarm is sounded, the bees only occupy the cells where the hives are located. At the end of the first minute, they occupy all grassy cells adjacent to hives (and still the hives themselves). At the end of the second minute, they additionally occupy all grassy cells adjacent to grassy cells adjacent to hives, and so on. Given enough time, the bees will end up simultaneously occupying all grassy cells in the forest that are within their reach. Neither Mecho nor the bees can go outside the forest. Also, note that according to the rules above, Mecho will always eat honey for an integer number of minutes. The bees catch Mecho if at any point in time Mecho finds himself in a cell occupied by bees. Write a program that, given a map of the forest, determines the largest number of minutes that Mecho can continue eating honey at his initial location, while still being able to get to his home before any of the bees catch him. 12.3.3 Segment Tree 1. (SPOJ, CPAIR) Given a sequence Ak of N (1 ≤ N ≤ 100, 000) non-negative integers (∀k, Ak < 1000), Answer Q (1 ≤ Q ≤ 100, 000) queries, where each query consists of three integers, v, a, b. The answer is the number of pairs of integers i and j such that 1 ≤ i ≤ j ≤ N , a ≤ j − i + 1 ≤ b, and Ak ≥ v for every integer k between i, and j, inclusive. 12.3.4 More Standard Graph Theory Maximum Flow and Variants Strongly Connected Components Other Graph Theory 1. (USACO Open 2008, nabor) Farmer John has N (1 ≤ N ≤ 100, 000) cows who group themselves into “Cow Neighborhoods”. Each cow is at a unique rectilinear coordinate, on a pasture whose x and y coordinates are in the range 1 . . . 1, 000, 000, 000. Two cows are neighbors if at least one of two criteria is met: (1) If the cows are no further than some integer Manhattan distance C (1 ≤ C ≤ 1, 000, 000, 000) apart. (2) If cow A and B are both 121 Hsiang, Wei, Liu Chapter 12. Problems neighbors of cow Z, then cow A is a neighbor of cow B. Given the locations of the cows and the distance C, determine the number of neighborhoods and the number of cows in the largest neighborhood. 2. (CF 260C) Andrew plays a game called “Civilization”. Dima helps him. The game has n (1 ≤ n ≤ 3 · 105 ) cities and m (0 ≤ m < n) bidirectional roads. The cities are numbered from 1 to n. Between any pair of cities there either is a single (unique) path, or there is no path at all. A path is such a sequence of distinct cities v1 , v2 , . . . , vk , that there is a road between any contiguous cities vi and vi + 1 (1 ≤ i < k). The length of the described path equals to k − 1. We assume that two cities lie in the same region if and only if, there is a path connecting these two cities. During the game events of two types take place: • Andrew asks Dima about the length of the longest path in the region where city x lies. • Andrew asks Dima to merge the region where city x lies with the region where city y lies. If the cities lie in the same region, then no merging is needed. Otherwise, you need to merge the regions as follows: choose a city from the first region, a city from the second region and connect them by a road so as to minimize the length of the longest path in the resulting region. If there are multiple ways to do so, you are allowed to choose any of them. Dima finds it hard to execute Andrew’s q (1 ≤ q ≤ 3 · 105 ) queries, so he asks you to help him. Help Dima. 12.3.5 Standard Computational Geometry 12.3.6 Less Standard Problems 12.4 Beyond 12.4.1 Data Structure Nonsense 12.4.2 Other Nonsense 122
Source Exif Data:
File Type : PDF File Type Extension : pdf MIME Type : application/pdf PDF Version : 1.5 Linearized : No Page Count : 136 Page Mode : UseOutlines Author : Title : Subject : Creator : LaTeX with hyperref package Producer : pdfTeX-1.40.16 Create Date : 2015:12:27 21:03:21-05:00 Modify Date : 2015:12:27 21:03:21-05:00 Trapped : False PTEX Fullbanner : This is pdfTeX, Version 3.14159265-2.6-1.40.16 (TeX Live 2015) kpathsea version 6.2.1EXIF Metadata provided by EXIF.tools