Let and be two sets of integers. We say that is a multiple of if, for every integer , there is an integer such that is a multiple of (that is, for some integer ).
We are given four integers such that . Let be the set of integers . We would like to find the smallest subset of that is also a multiple of . Output the size of this set .
Note: is a subset of , and is a multiple of , so a solution always exists.
- Set union
- Bitset
- Greed
- DP
- Graph (edges from divisors to multiples)
- Closure under an operation
- Intervals
- Matrix
- Primes, Prime Factorization, Seive
- SQRT factorization and SQRT tricks
- Tree
- Want logarithmic time solution
Notation: We will use to denote the given set, and to denote the "goal" or optimal set that satisfies the constraints. We say we "choose" (or "select") an integer in an algorithm if it results in when the algorithm is completed.
IdeaCharacterization and Greedy AlgorithmNO
When I first read this problem, I tried to look at examples in order to find a pattern. This problem deals with contiguous intervals and of divisibility of numbers; both of these subjects imply a high degree of "structure" to the problem. So the first goal was to try to understand this structure.
Based on the initial observations and looking at examples, we come to the following (pretty easy) observation.
For any integer in our set , we only need to use the largest multiple of when choosing (our optimal goal set).
If some integer is chosen, but there is some other integer so that and is a multiple of , then we can simply replace with , and we get another valid solution.
This observation is fairly easy to come up with, and it leads to a fairly simple "greedy" algorithm to construct a solution.
<code class="py">
Let T = {}
for x = D downto 1:
if (A <= x<=B or C<=x<=D):
if (no multiples of x exist in T):
add x to T
</code>That is, we consider the in descending order, and we greedily "select" a number if there is no other multiple of already taken in . Obviously, since , this algorithm is completely inefficient. But it does yield a good characterization of the optimal solution . We can now use this to prove the correctness of other ideas/algorithms, by showing that they return a set that is equivalent to the one returned by this solution.
Often, it is easy to prove that something "greedy" works. Once we have that, we know that this train of thought will likely be fruitful and that we should avoid trying other approaches (for example, DP). The hard part is to understand the structure of the problem well enough to find a nice efficient solution.
Now that we know that a greedy solution works, we need to exploit patterns and structure in the problem in order to find an efficient greedy algorithm. If we were given an arbitrary set , this problem would be extremely hard (I suspect); but since has a nice structure (the union of two intervals), I expect that we can use this to more easily find the optimal solution. The following ideas will attempt to exploit this.
IdeaDividing IntervalsTLE
In Idea 1, we were able to characterize how the optimal would look, and we came up with a greedy (but slow) way to construct . Next, we inspect this algorithm and try to learn more about the structure of the problem. By inspection (especially by looking at examples), we come up with another neat observation.
Every element in must be chosen.
If and then any multiple (if ). So, no larger multiples of are in , and so must be chosen in to satisfy the definition of "set multiples".
This observation may or may not be considered "obvious", but it's easy to see if you try to run the greedy algorithm from Idea 1 on any reasonable example. So it's good to look at examples.
For me, I had some intuition about these types of problems, so this observation came pretty naturally to me. See the Related Problems section for other problems related to divisibility. Gaining experience in dealing with "number-theoretic" problems will help in the future.
Moving on, we can make similar observations to this one. We can ask the questions: "What if ? What if ?".
If then the optimal algorithm will choose all of the elements between and . Moreover, no other elements need to be chosen.
This really is a corollary to the first Key Observation. If , then we already know that all numbers between and must be chosen (this is precisely the Key Observation).
We now show that the greedy (optimal) algorithm (from Idea 1) would not choose any other elements. In particular, we show that for all , there is an integer between and that is a multiple of . (Note: this is not very hard to prove, and the reader is encouraged to do so independently.)
Take any integer . Then consider the largest such that . Then, by construction, we have that (since was the largest multiple smaller than this). And since and , we therefore have that . So this multiple is precisely in the range of already chosen numbers. Hence always has a multiple in this range.
Thus, the Optimal Greedy Algorithm would never select another number. So this is optimal.
We have just shown that if then the optimal solution (i.e.: the algorithm above) will select exactly those numbers between and , which will suffice.
What if ?
In these cases we must select all of (since none of these are multiples of each other), but we may have to select some additional numbers between and . Again, we argue that we would only need to select numbers greater than but not greater than (since all numbers have some multiple in that range). But even so, we don't select them all, because many of these will have some multiple in the already chosen set between and .
In the case where the previous observation implies that we can simplify the problem. In particular, the problem reduces to being able to find which numbers between and have some multiple between and .
I had the following idea for an algorithm intuitively. I wasn't sure whether it would work, but it seemed like a natural thing to try.
Since these are contiguous intervals ( and ), here is one way to proceed.
For a fixed integer , we consider the interval . It turns out that every integer in this range has a multiple in the original range () (Proof omitted). Using a form of "Brute-force", we therefore "try all possible ". Consider , then , ..., and so on, keeping track of which of these intervals overlap with , and marking all integers in these intervals as "factors" / "not-chosen" (Implementation details omitted).
Assuming we figure out the implementation details, this will yield an algorithm that correctly marks all numbers in that have a multiple in . By taking the union of and , minus the marked numbers, we will have all the numbers that would be chosen in an optimal solution.
In our Problem Simplification, we said we were looking for all the numbers in that have some multiple in . The above algorithm, however, solves this problem in reverse: we consider all numbers in the range and find all factors of them within the ranges of .
This is an example of "working backwards". We are looking at the inverse problem, which can sometimes be easier to solve.
How efficient is this? How large can get in the above algorithm?
At this point, I tried some examples. A very nice example turned out to be , and can be anything reasonable. (I encourage the reader to try this with increasing values of ). We realize we don't have a bound on , except that . But we do notice after a while that these "intervals" stop being disjoint.
For example, take and then . For , the interval is which is . But for (omitting the calculations), we get the interval . Notice, the upper-bound () of the second interval overlaps with the lower-bound () of the first interval. The reader may verify that this continues to be the case for all for this example (I only tried until ). This yields a very nice pattern: for large enough , there is some such that all numbers are covered by the numbers in the intervals , over all .
This observation is pretty nice, because it shows that, after a while we can just ignore all small numbers (and assume they have a multiple). When does this happen exactly?
Well we want to know when overlaps with . This happens if and only if . For simplicity, we relax the "floor" and "ceiling" symbols, and just solve the inequality directly, and we get:
So, in particular, whenever , the intervals will be overlapping. Repeating this argument (essentially by induction) shows that, once this happens, this set of intervals will cover all numbers from some down to 1.
This algorithm works well whenever is large in comparison to .
Here are the implementation details fleshed out into pseudo-code. (Note: This is equivalent to Algorithm 2 above)
<code class="py">
SetMultiples1(A,B,C,D):
if C <= D/2:
# (Implicitly) Set T = {D,D-1,D-2,...,floor(D/2) + 1}
return ceil(D/2) # We only want the size of T
else:
Let marked = {} be an empty set
# The original ranges that define S
Let upperRange = (C,D)
Let lowerRange = (max(floor(B/2) + 1,A), B)
for all k = 2..INFINITY:
currentRange = (ceil(C/k), floor(D/k))
prevRange = (ceil(C/(k-1)), floor(D/(k-1)))
if intersection(currentRange, prevRange) isnt EMPTY: # They overlap
marked.insert(x) : for all x from floor(D/k) downto 1
break
else:
marked.insert( intersection(currentRange, lowerRange) )
return upperRange.size() + lowerRange.size() - marked.size()
</code>In the above pseudo-code we represent intervals as (lower bound, upper bound ) pairs. For example, the variables upperRange, lowerRange, and currentRange all represented intervals.
As proven earlier, this algorithm works fine when is large. Eventually the intervals will begin to overlap, and we can break out of the loop early. In the next Idea Section, we show how to handle the case when is small
IdeaDirect FactorizationAC
In the idea section above, we described an algorithm that is guaranteed to terminate quickly whenever is much smaller than (that is, whenever is large). We won't review that algorithm now, but we recall that it runs in time-complexity. We would now like to answer the following question:
How can we solve the problem when is closer to ?
As an example, we can consider the extreme case when . Can we think of a good algorithm in this case?
If then we take and consider all the elements between . Similarly to as proven (in the previous Idea Section), we only need to take items greater than but not greater than . Of all these numbers, we can ignore any of those that are factors of . This would yield the optimal set (since all numbers chosen are their own "maximal" multiples; see Idea Section 1).
How efficient would this be?
This analysis of efficiency requires some familiarity with number theory and factorization. In particular, one should recall the "Square-Root Trick" for factorization, that shows that it takes time to find all factors of a number . In our case, since , we check numbers.
Also, the number of actual factors is much smaller than this. Most numbers under have under 100 factors (this is a rough estimate).
So, overall, this is reasonable.
The algorithm works in general. Given any and , we can manually factorize all numbers between and , marking each factor as needed. We can then take the intervals and and subtract any marked numbers. This will yield an optimal solution.
The overall time complexity of this is , since we have to manually check the factor of numbers, which takes time each.
This algorithm runs quickly if is small.
In the previous Idea, we found an algorithm that is efficient whenever is large. We have also shown that the Algorithm from this current Idea is efficient whenever is small. From experience (e.g.: from Calculus) or by "Symmetry" we know that it's best to find a "balance" between these two solutions (i.e.: get their efficiencies as close to each other as possible).
This is similar to the "Square Root Trick" for factorization: given two numbers that multiply to , at least one of the numbers must be smaller than , etc. So we can factorize in time. This principle applies very often when solving problems: we often have two conflicting functions we want to optimize and we can prove that one of those functions will be small whenever the other is large. Usually if these are functions in some number , one of the functions will be or something similar.
Anyway, with this in mind, we have the following problem.
Suppose we treat is a constant and let be variable. In particular, consider the difference .
Recall that we have:
- an algorithm that runs well when is large (in time)
- another algorithm that runs well when is small (in time)
Then we can combine these algorithms to get a (worst-case) algorithm.
To "combine" these algorithms, we mean: given a and , we choose whichever algorithm is faster for our particular input (). If we do this combined algorithm, we get a time complexity defined as follows:
For simplicity, let's assume that is a constant and we vary . So we let be a variable, and we can write all of these functions in terms of . So we really have:
Now we are taking the minimum of two functions in , one that is increasing and one that is decreasing, and we would like to find out "how bad can this get". In particular, we want to find the maximum point of this function. Using techniques from calculus, or just by intuitive reasoning, we know this is maximized whenever the two are equal. Hence, we can solve for by setting:
Solving for (math omitted), we get that . This means, whenever the difference is more than , we choose the first algorithm; and whenever the difference is less than , we choose the second algorithm. In the worst case we will have:
So, altogether we have a algorithm, as desired.
The lemma above actually gives us an algorithm for solving the problem, based on the two different concepts we have developed. See the Solution Summary (in the Review Section) below for a concise overview of the solution.
Review
Summary
All-in-all, this problem required some interesting greedy observations to solve it efficiently.
We first observe that, for a given integer , we "choose" to be in our final set if and only if has no larger multiples in (for simplicity, we will call all of these numbers "maximal" numbers). This always yields an optimal solution (proven above). Hence, we simply want to find all "maximal" numbers in the ranges or .
To find an efficient algorithm, we can exploit the fact that consists of two disjoint intervals. This actually makes the problem much simpler and more "regular". For example, we can make the following observation: Between and (inclusive), we always need to select the numbers above (since none of these are "maximal"), if . In fact, with a bit more work, we can show that, if then these are the only numbers we need to select (we can prove that any smaller number has a multiple in this set). So we can assume that . Similarly, we can assume that , since an analogous truth holds for and .
At this point, we can assume is somewhat "close" to (in particular, ). To solve this, I tried one fairly intuitive algorithm: Look at the end-points of the interval and divide them by 2. Then every integer in this new interval has a corresponding multiple in the original interval . So all such numbers are clearly not maximal. A similar fact holds for , , ..., and so on. So, the algorithm is to try all these intervals, and "mark" the numbers that we find. One convenient fact about this is that most of these intervals are disjoint, so we don't need to explicitly mark the , but we just count how many are in the range and also in the range . When this algorithm terminates, the size of our final set will be (the numbers in our given ranges, minus the number of "marked" factors).
By inspection, the intervals will eventually intersect with each other. Actually, you can prove that, after a certain , the intervals will cover all numbers from down to 1. So we can actually "exit" early. This happens whenever (we proved this in Idea 2), so it happens more quickly whenever is large (or when is small, but we can't assume this).
By noting that this algorithm works when is large, we can ask the next intuitive question: "What about when is small?". To answer this, we realize that we can just simply explicitly find all the factors of the numbers between and . We mark these factors that overlap with the chosen part of the interval. This runs in time, since there are about numbers in the range and each number requires time to find its factors. So, this algorithm is clearly better when is smaller.
We now have two "complimentary" algorithms, the first having time-complexity, and the second having time-complexity. We can combine these algorithms by always choosing the "minimum" one; that is, we choose whichever algorithm runs faster for our given input. We then have an algorithm that has time-complexity: . Using some intuitive arguments (or by calculus) this function has its worst-case complexity of when . Since this just (barely) runs correctly under the time-limit for TopCoder.
Accepted.
-
We select an integer to be in our set if and only if it has no other multiples in the set . This was pretty intuitive to come up with, but it was helpful when trying to find a fast algorithm.
-
Given a single interval we MUST select the numbers greater than in an optimal solution .
-
If we take an interval of integers from to , and we divide both end-points by an integer , then every integer in this new interval () will have a multiple in the original range. This formed the basis of our first algorithm
-
Take an interval of integers from to . Consider the intervals , , , ..., , . Then will overlap with if and only if . This showed that we could quit this algorithm early whenever gets large enough. In particular, we can end earlier if is larger.
-
It takes time to find all the factors of a number. Hence, all factors of all numbers between and can be checked in time. This gave us the basis of our second algorithm. This works whenever is small.
-
Consider the time-complexity function . This function achieves its maximum (worst-case) value of when . Since is decreasing and is decreasing, we can show that the worst-case point occurs when these two functions are equal. So we set and solve for . This gives us the formula.
-
This is largely a greedy / ad-hoc problem. These kinds of problems usually require finding good observations about the structure of the problem. It often helps to look at examples to help find these patterns. Also, once we started to find more and more observations, it was good to go back and try them on examples to get a good understanding of how they work.
-
Once we found a greedy algorithm, we realize that we can make it efficient by exploiting the structure of the problem. In particular, the input set will always consist of two disjoint intervals. (This might have been much harder if we were given an arbitrary set .) Another way to exploit the conditions is to notice that . This implies that a algorithm should be possible, since is right around a "reasonable" input-range.
-
If you've never seen "the Square Root Trick" for factorization, then this problem becomes much harder. In addition (see the Learning Points), I've learned from experience that this "Square Root Trick" applies (in principle) to other kinds of problems. This helped us to analyse the running time achieved when combining the two algorithms. You also need some experience and intuition in basic Number Theory in general to be able to solve this problem.
-
A nice example was the case when . This was an extreme case (i.e.: when was 0). By finding an algorithm that works here, we could easily generalize it to work for other small .
-
Most of the algorithms we came up with were pretty "intuitive". The way we solved this problem was by writing out these algorithms ("testing" them), and by finding out exactly where they fail. In particular, we learned that these algorithms largely depended on the parameter , so then we could solve the problem by focusing on this variable.
-
Instead of finding all multiples of a given number , we focused on finding the factors of numbers .
-
The "Square-Root" Trick with Minimax Functions. In general, whenever you have two symmetric, competing "resources", you usually can achieve optimality (either maximizing or minimizing some function of these resources) by setting them equal. When factorizing a number into factors and such that , this principle applies because is maximized whenever . This is also the worst-case amount of numbers you would need to check to find all the factors. Or for example, if you have a function , this is minimized when (I think). This also forms the basis for "Square Root Decomposition" algorithms which arise in data structures (see the Related Problems section below).
-
Number Theory / Divisibility / Factorization / Square Root Trick
-
Greedy / Observations / Exploit Structure and Conditions
-
Brute Force / Brute Force with Greedy Optimizations
-
Math / Formulas and Proofs
-
SQRT-Decomposition
Given unique numbers between and (for some positive integer ), prove that there exists a pair of elements and such that is a multiple of . This is a nice math problem that I remembered when reasoning about divisibility.
UVa 11466 - Largest Prime Divisor. A problem on finding large prime factors. Take note of the constraints and recall out "Learning Point".
UVa 11960 - Divisor Game. This problem shows that most numbers have a small number of factors. It's also a nice illustration of working backwards.
Codeforces Round #257 (Div. 1) C - Jzzhu and Apples. Another problem dealing with number theory and divisibility. It is also an "optimization" problem where we are trying to maximize something with respect to the numbers.
NCPC 2008 Problem J: Just a Few More Triangles! Here is a harder number theory problem that I don't know how to solve. It's more practice. (I found this problem on Codeforces Gym)
Coding Contest Byte: The Square Root Trick. A nice article describing Square Root decomposition and related problems.