banished_prince's blog

By banished_prince, history, 5 months ago, In English

Hi guys!

Today, we will see how to solve this problem:

You are given two positive integers n and x. Find an array of positive integers with length n and sum x that has minimum LCM.

Let's start with a simple brute force solution that creates every possible valid array and finds the minimum LCM:

void solve_1(int len, int sum, int cur, int& ans) {
    if (len == 0) {
        if (sum == 0) {
            ans = min(ans, cur);
        }
        return;
    }

    int rem = sum - len + 1;
    for (int val = 1; val <= rem; val++) {
        solve_1(len - 1, sum - val, lcm(val, cur), ans);
    }
}

int ans = x;
solve_1(n, x, 1, ans);

This solution is currently able to solve all valid inputs, constrained up to (x <= 25). In order to understand how to optimize it further let's see what kind of arrays it generates:

[1, 2, 2, 4]
[1, 2, 4, 2]
[4, 2, 1, 2]
[4, 2, 2, 1]

It's a case of (n, x) = (4, 9) and as you can see these are anagrams to each other. So, to remove these kind of duplicates we will only generate non-decreasing arrays and it's our first optimization. Next, let's see some more examples to understand what else we can do:

[9]
[1, 8]
[3, 3, 3]
[1, 2, 2, 4]
[1, 2, 2, 2, 2]

Here, one thing we can observe is that the LCM is always present and will be the maximum element of the array. What we are looking here is, say LCM is k and we replace every element with it then:

k * n >= x
k >= ceil(x / n)

Similarly, we can say that the upper bound of the answer will be:

v = floor(x / n);

[v, v, v + 1, v + 1] --> [1, 1, 2 * v, 2 * v]
[v, v, v, v + 1] --> [1, v, v, 2 * v]
[v, v + 1, v + 1, v + 1] --> [1, 2, v, 2 * v]

k <= 2 * v

Basically, we can always create a solution with this upper bound. And this is gonna be our second optimization. We will make sure in our recursion to only create the arrays that have lower LCM than our current answer which is initially our upper bound:

void solve_2(int len, int sum, int pre, int cur, int& ans) {
    if (len == 0) {
        if (sum == 0) {
            ans = min(ans, cur);
        }
        return;
    }

    for (int val = pre; val * len <= sum && val <= ans; val++) {
        int nxt = lcm(val, cur);
        if (val * len == sum) {
            ans = min(ans, nxt);
            continue;
        }
        if (nxt >= ans) continue;
        solve_2(len - 1, sum - val, val, nxt, ans);
    }
}

int upper = (x / n) * 2;
int ans = upper;
solve_2(n, x, 1, 1, ans);

With these two optimizations, we have improved the constrained on our algorithm to solve up to (x <= 150).

This is the best I can think of a recursively optimized solution. From here we start with an iterative solution, first let's take a look at an example:

k = lcm(a)
a = [divisor(k), divisor(k), k, k]

So, all of the array elements must be the divisor of k in order for k to be LCM. Then let's fix this k first and try to create an array with sum exactly x and length exactly n:

bool check_1(int len, int sum, int val) {
    vector<bool> dp(sum + 1);
    dp[0] = true;
    for (int i = 1; i <= len; i++) {
        vector<bool> tp(sum + 1);
        for (int j = 0; j < sum; j++) {
            if (dp[j]) {
                for (int divisor : divisors[val]) {
                    if (j + value > sum) break;
                    tp[j + value] = true;
                }
            }
        }
        dp = tp;
    }

    return dp[sum];
}

It's a dp to check if we can create a sum x by using exactly n divisors of k. The below example shows the dp indices marked as true during each iteration for (n, x, k) = (2, 6, 4) were:

[0] --> [1, 2, 4] --> [2, 3, 4, 5, 6, 8]

As 6 is present in final array so it's true that we can form it. Also as we have lower and upper bound on k so the range that we need to check is (x / n). So the current solution's time complexity will be:

O((x / n) * (x * n) * cbrt(x / n)) --> O(x * x * cbrt(x / n))

Now, you can say that this dp solution is somewhat like knapsack, but there is an issue that unlike a knapsack which doesn't have a constraint on the length of array you would have to create. So, here we need to modify our original representation of the array that we are creating:

k = lcm(a)
a = [divisor(k), divisor(k), k, k]
a_ = [k - divisor(k), k - divisor(k), 0, 0] --> [k - divisor(k), k - divisor(k)] --> [values(k), values(k)]
x_ = k * n - x

So, we will find this modified array with (length <= n). Let's say our initial array contains all elements as k, than this array b tells that for each index how much value you need to decrease from k to make it equal to sum x. Here, values represents:

values[k] = divisors[k];
values[k].pop_back();
reverse(values)

After this transformation, it becomes exactly a knapsack problem. Below represents one of the implementation of the knapsack:

vector<vector<int>> values;
vector<int> dist;

bool check_2(int len, int sum, int val) {
    int tot = min(sum, values[val].back() * (len / 2));
    fill(dist.begin(), dist.begin() + tot + 1, 0);

    queue<int> q;
    q.push(0);
    while (q.size()) {
        int cur = q.front();
        q.pop();

        if (2 * dist[cur] > len) continue;

        for (auto value : values[val]) {
            if ((sum - cur) % value == 0) {
                int can = (sum - cur) / value;
                if (can <= (len - dist[cur])) {
                    return true;
                }
            }
            if (value + cur <= tot && !dist[value + cur]) {
                dist[value + cur] = dist[cur] + 1;
                q.push(value + cur);
            }
        }
    }

    return false;
}

void solve_3(int len, int sum, int low, int& ans) {
    if (len * low == sum) return;

    for (int val = low; val <= sum; val++) {
        if (check_2(len, val * len - sum, val)) {
            ans = val;
            return;
        }
    }
}

for (int i = x; i >= 1; i--) {
    for (int j = 2 * i; j <= x; j += i) {
        values[j].push_back(j - i);
    }
}

int lower = ((x + n - 1) / n);
int ans = lower;
solve_3(n, x, lower, ans);

So, I forget to mention one important claim, i.e. in one of the optimal construction for this problem there is a pattern that a particular element is present in weak majority. What I mean is, if we have an optimal construction that doesn't full fill the above claim we can always transform it without breaking the constraint of (n, x, k). Below is one example of it:

// Transformation
[1, 1, 2, 4, 4] --> [2, 2, 2, 2, 4]

// Weak Majority
[1, 2, 4, 4]

Now, I didn't have a formal proof, but we can utilize this fact as when we are constructing our array we can perform a simple check that if the current element can be used for the rest of the array. After this check we have made sure that we can find an answer in just half of the original length n.

Time complexity of this approach is shown below:

O((x / n) * (x * cbrt(x / n)))

Also, it can solve with constrained up to (x <= 2000). You can find the source code here.

Thanks for the read!

Full text and comments »

  • Vote: I like it
  • +13
  • Vote: I do not like it

By banished_prince, history, 5 months ago, In English

Hi guys!

So, if you have given the recent contest, you may have encountered Problem E which is a construction problem, well let's try a little different version of it.

You are given a positive integer n. Find a permutation that does not contain any bad indices or say it doesn't exist.

Now, the intuition here is first start with a base solution and slowly make it better and see if it solves the problem. Here, the base solution is the one mentioned in the editorial. Let's see how it looks:

[_, 2, 2, _, 2, 2, _, 2, 2, _, 3, 3, _, 3, 3, _, 3, 3, 2, 3, _, _]

Here, 2 and 3 represents the spf value of numbers present at those indices. _ represents remaining numbers.

The above is just one representation that shows two issues with this construction. The first one is putting the remaining 2 at the end of the sequence which makes for one bad index in below example_1:

[1, 2, 4, 5, 6, 8, 7, 10, 12, 11, 14, 16, 13, 18, 20, 17, 3, 9, 19, 15, 21, `22`, 23]

The solution is simply to put it with rest of the sequence of 2 such that it looks:

[_, 2, 2, _, 2, 2, _, 2, 2, 2, _, 3, 3, _, 3, 3, _, 3, 3, 3, _, _]

So, our example_1 will change to this:

[1, 2, 4, 5, 6, 8, 7, 10, 12, 11, 14, 16, 13, 18, 20, `22`, 17, 3, 9, 19, 15, 21, 23]

This is one of the solution having bad indices upper bound of 1 which is minimum you can get. But still not enough to solve the task above. The second issue is to resolve the remaining one bad index [22, 17, 3] which can be done by swapping the 22 with a number divisible by 3 that occur before it, like 18. So the sequence will look like:

[1, 2, 4, 5, 6, 8, 7, 10, 12, 11, 14, 16, 13, `22`, 20, `18`, 17, 3, 9, 19, 15, 21, 23]

Now, the above bad index issue is resolved so it should solve the problem, well not quite take a look at the below example_2:

[1, 2, 4, 5, 6, 8, 7, 10, 12, 11, 14, 16, 18, 13, 3, 9, 15, 17, 19]

In this the bad index is [15, 17, 19] and as you can see this example_2 does not have the example_1's bad index issue and it's a pattern I have seen but couldn't prove, also it repeats after every 12 values of n starting from 19. Also, when this pattern of bad index will occur there are two triplets like [14, 16, 18] and [3, 9, 15] will occur at the end of sequence 2 and 3 that looks:

[_, 2, 2, _, 2, 2, _, `2, 2, 2`, _, 3, 3, _, 3, 3, _, `3, 3, 3`, _, _]

We need to utilize these, the idea is to pick the last value from these triplet's and transform the sequence like this:

[1, 2, 4, 5, 6, 8, 7, 10, 12, 11, 14, 16, 13, 3, 9, 17, `15`, `18`, 19]

It resolved the example_2's bad index but introduce the example_1's bad index which can be resolved as mentioned above. So does it solves the problem for all values of n. Almost all except for these 3, 5, 7, 8. So does these values did not have an answer:

[1, 2, 4, 5, 6, 3, 7]
[1, 2, 4, 5, 8, 6, 3, 7]

Except for 3 and 5. Basically, brute force it to find them. And that solves it. Here's the Submission.

Full text and comments »

  • Vote: I like it
  • +4
  • Vote: I do not like it