Combinatorics Approach 2x+ times faster than DP
Intuition\nFirst, we simplify the input into a series of transitions up and down by some amounts. III becomes a transition with +3 and DDDDD represents one with
# Intuition\nFirst, we simplify the input into a series of transitions up and down by some amounts. III becomes a transition with +3 and DDDDD represents one with -4. Here is a representation of "DDIIIIDDD" with 3 transition points.\n\n![2.jpg](https://assets.leetcode.com/users/images/503e2c08-cffa-4cff-9574-ec96bf564f83_1713567874.347818.jpeg)\n\nThis approach is based on an important observation of the relation between the solution at transition i to i+1.\n  \n\nTo get from T1 to T2 we would need to start with a value that could decrease by 2. T2 to T3 increase by 4 and T3 to End decrease by 3.\n  \n\nWe have the numbers 0-9 to possibly insert at each transition point. We couldn\'t go from T1 to T2 starting with 0 or 1 because there aren\'t two numbers below those. We can only have a starting value at T1 in the range 2-9 that can get to T2. Let \u0394 = the number of elements needed to move from T[i] to T[i+1]. If T[i] is increasing then all \u0394 numbers selected must be greater than the transition\'s start and less than if the transition is down.\n\nFor T3 we can only get to the end with a starting value from 3-9. If we get to T3 with a valid combination of 0-9 then we have only 3 remaining.\n\nLet\'s see how many ways to get from T3 to the end from the starting value X with Y being all the combinations of 0-9.\nHere are a few:\nIf X is 7 and Y is [0,1,2] there is one way [2,1,0]\nIf X is 7 and Y is [0,5,6] there is one way [6,5,0]\nIf X is 6 and Y is [0,1,2] there is one way [2,1,0]\nif X is 3 and Y is [0,1,4] there are no ways because 4 cannot be placed to the right of T3\'s 3 starting point.\n\nYou can try this with every combination and we can see that as long as all in Y are less than X there is one possible path to the end. Otherwise, there are 0.\n\nHere we get our key insight. If we reach T3 with 3 numbers to the less than of T3\'s start the answer is always 1 else 0. **The starting value at T[i] doesn\'t matter!** The only thing that influences the result from point T3 is the number to the left of the starting point. Similarly, if T[i] is decreasing then the count to the right is the only thing that matters.\n\n\nLet\'s go one step further to T2. When arriving at T2 there will have been 2 numbers used to get so 7 values remaining to choose from. If we arrive at T2 with at least 4 values to the right we can get to T3 otherwise there are 0 ways. To move from T2 to T3 we will need to consume 4 values greater than T2\'s starts. But if we arrive at T3 with less than 3 to the left there are no solutions. If for example, we know T2 has 6 values to the right and 1 to the left. When we move from T2 consuming 4 of those right values how do we know what value T3 would start with and how many it has to the left or right?\n\n![2.jpg](https://assets.leetcode.com/users/images/411253ba-95b1-4512-9721-6b31f393c5f7_1713564434.0778754.jpeg)\n\n  \n  \n\nThere are 3 possibilities for the remaining 2 values to the right of T2:\n\n1. Both are to the right of T3\n\n2. One is left and one is right\n\n3. Both to the left of T3\n\n![3.jpg](https://assets.leetcode.com/users/images/af9c5706-aefa-41f6-a99b-113554cfbf12_1713564552.6004643.jpeg)\n\n  \n  \n\nLooking at the cases from T3\'s perspective:\n\n1. We know T3 has 2 to the right and 1 to the left since T3 is greater than T2 and T2 is greater than the 1 to the left.\n\n2. Left = 2 and right = 1\n\n3. Left = 3 and right = 0\n\n  \n\nHey, we know the answers to all of those cases! Only case 3 leaves us with a single possible combination. This means that at point T2 we need to select 4 numbers and have 2 still to the left of T3. Since T3 is the end point we can\'t select that and it consumes one of the 6 to the right of T2. Then we have to fill in 3 "slots" from 5 numbers. T2 _ _ _ T3\n\n  \n\nHere is where combinatorics comes in. How many ways can we select 3 of the 5 numbers? That is expressed as 5 choose 3. Which can be calculated with a formula: Ck(n) = n!/(k!(n\u2212k)!) = 5!/(3!(5-3)!) = 10. We find there are 10 ways to go from T2 to T3 by choosing 4 numbers greater than T2\'s start that will give us the position where T3 has only 3 to the left. So for T2 with L = 1 R = 6 there are 10 solutions.\n\n  \n\nNow we don\'t know at T2 that T3 L=3,R=0 is the only solution. So try the case where T3 is L=2R=1 for example. There are now only 4 numbers between T2 and T3 so there are 4 choose 3 = 4 ways to make this traversal. And 3 choose 3 = 1 ways for T3 at L0R2.\n\n  \n\nThen the number of combinations for an increase from T2 to T3 with L=1 and R=6 = (5 choose 3) * T3(L3R0) + (4 choose 3) * T3(L2R1) + (3 choose 3) * T3(L1R2)\n\nEnding at the beginning, let\'s now look at T1 to T2 with the goal of T2 having L=1R=6. Since there are no values chosen to the left of T1 we have all 10 possible values to start with. Let\'s first try picking 8. This is T1 with L=8R=1. We are decreasing at T1 so we look to the left count. There are 8 and we need to choose 2 such that T2 has one to the left. Then there are 8 - (the value at T2 itself) - (the value to the left of T2) = 8-1-1 = 6 values to select from. Of those 6 values I need to choose 1. So there are 6 choose 1 = 6 ways to go from T1L=8R=1 to T2L=1R=6. And we know the result of that T2 state is 10. Since we have 6 ways to get to the 10 ways there are 60 solutions from T1L=8R=1. \n![5.jpg](https://assets.leetcode.com/users/images/32ccce90-c86a-4458-99a2-f99fec427f60_1713812973.1047883.jpeg)\n\n\nI want to take a step back and use actual numbers for demostration and proof this abstraction really works. We know of 0-9 that only "1" has one to the left so this is really about going from 8 -> ? -> 1 and how many ways are there to choose the middle such that it is less than 8 and greater than 1? [7,6,5,4,3,2] which is the 6 values we calculated. The only value possible in this scenario for T3L3R0 is 9. This path with the possible choices at each index then looks like this:\n![6.jpg](https://assets.leetcode.com/users/images/4c07b232-0bd8-45ab-8c67-ff8eb46fa41f_1713812982.6921813.jpeg)\n\nHow can we be sure that we use each element only once? Here is a chart breaking down each index and what values could be inside it. Below each choice in black is the total in that range. In blue is the number in that range we know have been consumed, but not which specifically. Then below that is the remaining choices. When we reach that final index we know from the remaing values under 9 (0..8) there have been 8 consumed and there is only one value available. One we\'ve chosen it then we have chosen all and only once.\n![7.jpg](https://assets.leetcode.com/users/images/65e972fc-e173-4e2b-b288-2d3f5ce20b02_1713814511.768981.jpeg)\n\nYou can then pick any value in the range at each point and remove it from the future ranges. Until you cross off all of them. Here is one path way were I choose a value in a black circle at each index and remove it from the future values. I satisfy the value changes and only use each value once.\n![final.jpg](https://assets.leetcode.com/users/images/f4a06b91-a674-4577-a2ba-62be7aba29ac_1713814666.6822524.jpeg)\n\nA minor optimization is that since L+R+(the index of T[i])=size we can derive the available right from the available L at T[i]. With the formula R=size-L-(index of T[i]). Making only the number on the left needed as input.\n\n\nIn summary, the combinations at T[i] (L) to T[i+1] is the sum of every T[i+1] (L) * the combination of choices from L to (L+R-\u0394) if increasing and 0 to (L-\u0394) if decreasing. If we sum all T[0] for every L in 0 to size we get the number of valid permutations without any duplicates. Yay.\n\nI had a lot of fun on this and decided to make my first solution post. Constructive feedback is appreciated.\n\n  \n\n# Approach:\n\nThe core functions numPermsDISequence, calcTransitionsFrom follow the above summary. The combinations calculation for each possible transition chooses with delta-1 and calculates the value to choose from as the delta + the index - 1. The -1 accounts for the ending value. Since our ranges spread out from the "center" point and delta away then each range gains an additional possible value at each step.\n\nSince the combinations calculations can take some computation I cache it even accross runs since x choose y is the same no matter what.\n\nFor calcCombinations(value, choose) I minimize the calculations required for the n!/(k!(n\u2212k)!) by selecting the smaller denominator and canceling out the pairs in the numerator.\n\nFor example 60 choose 6:\n60!/(6!(54!)) becomes (60*59*58*57*56*55)/(6*5*4*3*2*1)\n\nThen I use BigInteger to do the multiplication/division since the numerator could be very large.\n\n  \n\n# Results:\n\nI will compare my combinatorics approach to the dynamic programming approach given in "numPermsDISequenceDP". Both of which pass.\n\n  \n\nSince a big performance hit is calculating the combinations I run two types of tests. One with a fresh instance of Solution() each time and the second with a shared instance that has will calculate and cache the results as it goes.\n\n  \nHere are the benchmark results for both approaches given the same input for each comparison.\n\n  \n\n##### Test #1 Total time taken for 200 runs each with a random 500 length string:\n\n- No cache:\n    - Combinatorics: 9873ms\n    - DP: 24005ms\n    - Improvement 14132ms\n\n- Caching:\n    - Combinatorics: 9954ms\n    - DP: 24317ms\n    - Improvement: 14363ms\n\n  \n\n##### Test #2 Total time taken to run a single 200 length test 200 times:\n\n- No cache:\n    - Combinatorics: 842ms\n    - DP: 1704ms\n    - Improvement 862ms\n\n- Caching:\n    - Combinatorics: 713ms\n    - DP: 1699ms\n    - Improvement 986ms\n\n\n##### Test #3 Total time taken for random strings with sizes from 1 to 1000:\n- Combinatorics: 100201ms\n- DP: 224077ms\n- Improvement 140,576ms\n\n![Combinatoris VS DP 1-_1000 length strings.png](https://assets.leetcode.com/users/images/e071fd27-cdff-419c-8fa3-450f012ea69f_1713815661.975585.png)\n\n  \n\n# Complexity\n\n- Time complexity:\n\nIn the worst case # Transitions = n (ex: "IDIDID...")\n\nThen for each transition, we check all combinations possible which in the worst case is n again.\n\nSo the complexity is $$O(n*n)$$ same as the DP solution just a slower increase.\n\n  \n\n- Space complexity:\n\nThe combination caching takes up a constant 200x200 Long\n\nThe transitions caching would be n * 200 Long\n\n  \n\n# Code\n\n```\nclass Solution() {\n    private val modulo: Long = 1000000007\n    private val maxLength = 200\n    private val combinationsCalculator = CombinationsCalculator(modulo, maxLength)\n    fun numPermsDISequence(s: String): Int {\n        if (s.length <= 1) return 1\n        if (s.length > maxLength) throw IllegalArgumentException("Cannot exceed $maxLength max length")\n        val transitions = parseTransitions(s)\n        var perms: Long = 0\n\n        for (i in 0..s.length) {\n            val permsFor = calcTransitionsFrom(\n                i,\n                s.length,\n                0,\n                transitions\n            )\n            perms = (perms + permsFor) % modulo\n        }\n\n        return perms.toInt()\n    }\n\n    private fun calcTransitionsFrom(\n        availableLeft: Int,\n        size: Int,\n        transitionNumber: Int,\n        transitions: Array<TransitionPoint>\n    ): Long {\n        if (transitionNumber == transitions.size) return 1\n        val currentTransition = transitions[transitionNumber]\n        val availableRight = size - availableLeft - currentTransition.spacesToLeft\n        var result = currentTransition.getTransitionFor(availableLeft)\n        if (result == null) {\n            val nextPositions = currentTransition.nextPositions(availableLeft, availableRight)\n            var sum = 0L\n            nextPositions.forEachIndexed { index: Int, nextLeft: Int ->\n                val nextStepTransitions = calcTransitionsFrom(\n                    nextLeft,\n                    size,\n                    transitionNumber + 1,\n                    transitions\n                )\n                if (nextStepTransitions != 0L) {\n                    val combinationsForRange = combinationsCalculator.combinations(\n                        currentTransition.toFill + index - 1,\n                        currentTransition.toFill - 1\n                    )\n                    sum =\n                        (sum + (combinationsForRange * nextStepTransitions) % modulo) % modulo\n                }\n            }\n            currentTransition.setTransitionsFor(availableLeft, sum)\n            result = sum\n        }\n\n        return result\n    }\n\n    private fun parseTransitions(s: String): Array<TransitionPoint> {\n        val size = s.length+1\n        var prev = 0\n        var index = 1\n        val result = mutableListOf<TransitionPoint>()\n\n        while (index < s.length) {\n            if (s[prev] != s[index]) {\n                val delta = (index - prev) * if (s[index] == \'I\') -1 else 1\n                result.add(TransitionPoint(delta, prev, size))\n                prev = index\n            }\n            index++\n        }\n        val delta = (index - prev) * if (s[index - 1] == \'I\') 1 else -1\n        result.add(TransitionPoint(delta, prev, size))\n        result.add(TransitionPoint(0, index, size))\n        return result.toTypedArray()\n    }\n\n    class CombinationsCalculator(modulus: Long, private val maxLength: Int) {\n        private val combinationsCache: Array<Array<Long?>> =\n            Array(maxLength) { Array(maxLength) { null } }\n        private val modulo = modulus.toBigInteger()\n\n        fun combinations(value: Int, choose: Int): Long {\n            if (value < choose) return 0\n            if (value == choose || choose == 0) return 1\n\n            var result = combinationsCache[value][choose]\n            if (result == null) {\n                result = calcValueChoose(value, choose)\n                combinationsCache[value][choose] = result\n            }\n            return result\n        }\n\n        private fun calcValueChoose(value: Int, choose: Int): Long {\n            val minDivisor = minOf(choose, value - choose)\n            val maxDivisor = maxOf(choose, value - choose)\n            val divisors = (2..minDivisor).toList()\n            val numerators = ((maxDivisor + 1)..value).toList()\n\n            val numerator = multiplyBigInt(numerators)\n            val denominator = multiplyBigInt(divisors)\n\n            return ((numerator/denominator) % modulo).toLong()\n        }\n\n        private fun multiplyBigInt(values: List<Int>): BigInteger {\n            var result = BigInteger("1")\n\n            for (value in values) {\n                result *= value.toBigInteger()\n            }\n\n            return result\n        }\n    }\n\n    class TransitionPoint(val delta: Int, val spacesToLeft: Int, val size: Int) {\n        private val transitionsFrom: Array<Long?> = Array(size) { null }\n        val toFill = delta.absoluteValue\n        private val isIncrease = delta >= 0\n\n        fun getTransitionFor(availableToLeft: Int): Long? {\n            return transitionsFrom[availableToLeft]\n        }\n\n        fun setTransitionsFor(availableToLeft: Int, transitions: Long) {\n            transitionsFrom[availableToLeft] = transitions\n        }\n\n        fun nextPositions(availableLeft: Int, availableRight: Int): IntProgression {\n            return if (isIncrease) {\n                availableLeft..(availableLeft + availableRight - toFill)\n            } else {\n                (availableLeft - toFill) downTo 0\n            }\n        }\n    }\n}\n```
['Kotlin']