|
| 1 | +{ |
| 2 | + "cells": [ |
| 3 | + { |
| 4 | + "cell_type": "markdown", |
| 5 | + "metadata": {}, |
| 6 | + "source": [ |
| 7 | + "## Technique - Interval Sweep\n", |
| 8 | + "\n", |
| 9 | + "This is a cool trick I learned from [@lee215](https://leetcode.com/problems/maximum-sum-obtained-of-any-permutation/discuss/854206/JavaC++Python-Sweep-Line) on LeetCode for efficiently handling some types of overlapping interval problems. \n", |
| 10 | + "\n", |
| 11 | + "### Problem\n", |
| 12 | + "Suppose we're given an array `ranges` where each element represents the start and end value of a range of sequential integers; `[1, 5]` stands for `[1,2,3,4,5]`. For each integer in each range, return a count of the number of ranges it is in.\n", |
| 13 | + "\n", |
| 14 | + "**Example 1:**\n", |
| 15 | + "```\n", |
| 16 | + "Input: ranges = [[1,3], [2,4]]\n", |
| 17 | + "Output: {1:1, 2:2, 3:2, 4:1}\n", |
| 18 | + "Explanation: [1,3] means [1,2,3], and [2,4] means [2,3,4]. 1 is in one range, 2 is in two ranges, \n", |
| 19 | + "3 is in two ranges, and 4 is in one range. \n", |
| 20 | + "```\n", |
| 21 | + "**Example 2:**\n", |
| 22 | + "```\n", |
| 23 | + "Input: [[1,3], [1,4], [2,4], [2,5]]\n", |
| 24 | + "Output: {1:2, 2:4, 3:4, 4:3, 5:1}\n", |
| 25 | + "Explanation: Our ranges are [1,2,3], [1,2,3,4], [2,3,4], [2,3,4,5]. 1 is in one range, 2 is in four ranges, etc. \n", |
| 26 | + "```\n", |
| 27 | + "\n", |
| 28 | + "Some constraints:\n", |
| 29 | + "- `0 <= len(ranges) <= 10**5`\n", |
| 30 | + "- For every `ranges[i]`:\n", |
| 31 | + " - `0 <= ranges[i][0] <= ranges[i][1] <= 10**5`\n", |
| 32 | + " - `len(ranges[i]) == 2`\n", |
| 33 | + "\n", |
| 34 | + "### Brute force approach\n", |
| 35 | + "Using a counter, we could iterate through every value in every range and increment its frequency. This approach is `O(n^2)`; our worst case is `0 <= n <= 10**5` ranges each covering `0 <= n <= 10**5` values." |
| 36 | + ] |
| 37 | + }, |
| 38 | + { |
| 39 | + "cell_type": "code", |
| 40 | + "execution_count": 28, |
| 41 | + "metadata": {}, |
| 42 | + "outputs": [ |
| 43 | + { |
| 44 | + "name": "stdout", |
| 45 | + "output_type": "stream", |
| 46 | + "text": [ |
| 47 | + "1: 2\n", |
| 48 | + "2: 4\n", |
| 49 | + "3: 4\n", |
| 50 | + "4: 3\n", |
| 51 | + "5: 1\n" |
| 52 | + ] |
| 53 | + } |
| 54 | + ], |
| 55 | + "source": [ |
| 56 | + "from collections import Counter\n", |
| 57 | + "\n", |
| 58 | + "def count_frequencies(ranges):\n", |
| 59 | + " frequencies = Counter()\n", |
| 60 | + " for start, end in ranges:\n", |
| 61 | + " for i in range(start, end+1):\n", |
| 62 | + " frequencies[i]+=1\n", |
| 63 | + " return frequencies\n", |
| 64 | + " \n", |
| 65 | + "for val, count in count_frequencies([[1,3], [1,4], [2,4], [2,5]]).items():\n", |
| 66 | + " print(f\"{val}: {count}\")" |
| 67 | + ] |
| 68 | + }, |
| 69 | + { |
| 70 | + "cell_type": "markdown", |
| 71 | + "metadata": {}, |
| 72 | + "source": [ |
| 73 | + "## Interval sweep technique\n", |
| 74 | + "\n", |
| 75 | + "Consider that each time we see a range, all values between the start and the end have are now present in one range (plus any others). We can only get around the quadratic complexity by skipping these middle values. Let's keep a Counter `bounds_counts`; for each range given, `bounds_count[range[0]] += 1` and `bounds_count[range[0]+1] -= 1` (this may sound weird, but hang on). \n", |
| 76 | + "\n", |
| 77 | + "If our ranges are `[[1,3],[2,4]]`, then the correct result would be `{1:1, 2:2, 3:2, 4:1}`. Using this strategy, `bounds_counts` looks like the following:\n", |
| 78 | + "```\n", |
| 79 | + "[1,3] -> {1:1, 4:-1}\n", |
| 80 | + "[2,4] -> {1:1, 2:1, 4: -1, 5:-1}\n", |
| 81 | + "```\n", |
| 82 | + "Then, if we initialize `frequency = 0` and `frequencies = {}`, and loop over `range(1,5)`, adding `bounds_counts[i]` to `frequency` each time a value is in `bounds_counts` and storing the result in `frequencies`, the following occurs:\n", |
| 83 | + "```\n", |
| 84 | + "val frequency frequencies\n", |
| 85 | + "- 0 {}\n", |
| 86 | + "1 1 (+1) {1:1}\n", |
| 87 | + "2 2 (+1) {1:1, 2:2}\n", |
| 88 | + "3 2 (+0) {1:1, 2:2, 3:2}\n", |
| 89 | + "4 1 (-1) {1:1, 2:2, 3:2, 4:1}\n", |
| 90 | + "5 0 (-1) same as above; don't include values with frequency 0. \n", |
| 91 | + "```\n", |
| 92 | + "so frequencies now corretly reflects the count of intervals covering each value, and calculates it in linear time. In code:" |
| 93 | + ] |
| 94 | + }, |
| 95 | + { |
| 96 | + "cell_type": "code", |
| 97 | + "execution_count": 37, |
| 98 | + "metadata": {}, |
| 99 | + "outputs": [ |
| 100 | + { |
| 101 | + "name": "stdout", |
| 102 | + "output_type": "stream", |
| 103 | + "text": [ |
| 104 | + "1: 2\n", |
| 105 | + "2: 4\n", |
| 106 | + "3: 4\n", |
| 107 | + "4: 3\n", |
| 108 | + "5: 1\n" |
| 109 | + ] |
| 110 | + } |
| 111 | + ], |
| 112 | + "source": [ |
| 113 | + "def count_frequencies(ranges):\n", |
| 114 | + " bounds_counts = Counter()\n", |
| 115 | + " for start, end in ranges:\n", |
| 116 | + " bounds_counts[start] += 1\n", |
| 117 | + " bounds_counts[end+1] -= 1\n", |
| 118 | + " bounds = bounds_counts.keys()\n", |
| 119 | + " first, last = min(bounds), max(bounds)\n", |
| 120 | + " \n", |
| 121 | + " count = 0\n", |
| 122 | + " frequencies = {}\n", |
| 123 | + " for val in range(first, last):\n", |
| 124 | + " count += bounds_counts[val] if val in bounds_counts else 0\n", |
| 125 | + " frequencies[val] = count\n", |
| 126 | + " return frequencies\n", |
| 127 | + " \n", |
| 128 | + "for val, count in count_frequencies([[1,3], [1,4], [2,4], [2,5]]).items():\n", |
| 129 | + " print(f\"{val}: {count}\")" |
| 130 | + ] |
| 131 | + }, |
| 132 | + { |
| 133 | + "cell_type": "markdown", |
| 134 | + "metadata": {}, |
| 135 | + "source": [ |
| 136 | + "## Example: [Maximum Sum Obtained of Any Permutation](https://leetcode.com/problems/maximum-sum-obtained-of-any-permutation/)\n", |
| 137 | + "\n", |
| 138 | + "There are two tricks for this problem:\n", |
| 139 | + "1. Whatever \"permutation\" we pick should have the largest numbers at the most frequently covered index. \n", |
| 140 | + "2. We should get the index frequencies using the interval sweep technique. " |
| 141 | + ] |
| 142 | + }, |
| 143 | + { |
| 144 | + "cell_type": "code", |
| 145 | + "execution_count": 50, |
| 146 | + "metadata": {}, |
| 147 | + "outputs": [], |
| 148 | + "source": [ |
| 149 | + "from collections import Counter\n", |
| 150 | + "from typing import List\n", |
| 151 | + "\n", |
| 152 | + "def interval_sweep(requests):\n", |
| 153 | + " \"\"\"\n", |
| 154 | + " Given a list of ranges, return an array\n", |
| 155 | + " where arr[i] equals the number of ranges\n", |
| 156 | + " including i.\n", |
| 157 | + " \"\"\"\n", |
| 158 | + " bounds_counts = Counter()\n", |
| 159 | + " first, last = float('inf'), -float('inf')\n", |
| 160 | + " for start,end in requests:\n", |
| 161 | + " bounds_counts[start] += 1\n", |
| 162 | + " bounds_counts[end+1] -= 1\n", |
| 163 | + " first = min(start,end,first)\n", |
| 164 | + " last = max(start,end,last)\n", |
| 165 | + "\n", |
| 166 | + " frequency = 0\n", |
| 167 | + " overlap_count = []\n", |
| 168 | + " for val in range(first, last+1):\n", |
| 169 | + " frequency += bounds_counts[val]\n", |
| 170 | + " overlap_count.append(frequency)\n", |
| 171 | + " return overlap_count\n", |
| 172 | + "\n", |
| 173 | + "\n", |
| 174 | + "class Solution:\n", |
| 175 | + " def maxSumRangeQuery(self, nums: List[int], requests: List[List[int]]) -> int:\n", |
| 176 | + " nums.sort(reverse=True)\n", |
| 177 | + " overlap_count = interval_sweep(requests) \n", |
| 178 | + " max_sum = 0\n", |
| 179 | + " for i, frequency in enumerate(sorted(overlap_count, reverse=True)):\n", |
| 180 | + " max_sum += frequency*nums[i]\n", |
| 181 | + " return max_sum % (10**9 + 7)\n", |
| 182 | + " \n", |
| 183 | + "s = Solution()\n", |
| 184 | + "cases = [\n", |
| 185 | + " ([1,2,3,4,5], [[1,3],[0,1]], 19),\n", |
| 186 | + " ([1,2,3,4,5,6], [[0,1]], 11),\n", |
| 187 | + " ([1,2,3,4,5,10], [[0,2],[1,3],[1,1]], 47)\n", |
| 188 | + "]\n", |
| 189 | + "for nums, requests, expected in cases:\n", |
| 190 | + " actual = s.maxSumRangeQuery(nums, requests)\n", |
| 191 | + " assert actual == expected, f\"{nums, requests}: {expected} != {actual}\"" |
| 192 | + ] |
| 193 | + }, |
| 194 | + { |
| 195 | + "cell_type": "code", |
| 196 | + "execution_count": null, |
| 197 | + "metadata": {}, |
| 198 | + "outputs": [], |
| 199 | + "source": [] |
| 200 | + } |
| 201 | + ], |
| 202 | + "metadata": { |
| 203 | + "kernelspec": { |
| 204 | + "display_name": "Python 3", |
| 205 | + "language": "python", |
| 206 | + "name": "python3" |
| 207 | + }, |
| 208 | + "language_info": { |
| 209 | + "codemirror_mode": { |
| 210 | + "name": "ipython", |
| 211 | + "version": 3 |
| 212 | + }, |
| 213 | + "file_extension": ".py", |
| 214 | + "mimetype": "text/x-python", |
| 215 | + "name": "python", |
| 216 | + "nbconvert_exporter": "python", |
| 217 | + "pygments_lexer": "ipython3", |
| 218 | + "version": "3.8.5" |
| 219 | + } |
| 220 | + }, |
| 221 | + "nbformat": 4, |
| 222 | + "nbformat_minor": 4 |
| 223 | +} |
0 commit comments