3
$\begingroup$

Statement:

I have two arrays of number $A$ and $B$ with size $n$, I want to choose a set of $k$ indexes (let's call it $s$) such that the $GCD(A_i) = 1$ and the $\sum_{i=1}^{k} B_i$ is minimum possible.

Constraints:

$1 \le k \le n \le 5000$.

$1 \le A_i, B_i \le 1000$.

Test case:

$n=5$, $k=4$

$A = (10, 2, 5, 7, 8)$

$B = (7, 9, 11, 2, 3)$

$answer = 21$ you can choose $s = (1, 2, 4, 5) $

since the $GCD (10, 2, 7, 8) = 1$ and $7 + 9 + 2 + 3 = 21$.

Note:

I tried a $dp$ approach with state $dp[i][k][gcd]$ but this is not good enough for these constraints.

I also tried to think of the brute force approach, and nothing comes to mind.

$\endgroup$
2
  • $\begingroup$ Where did you encounter this? Can you credit the original source? $\endgroup$
    – D.W.
    Commented Nov 15 at 8:06
  • $\begingroup$ What does "not good enough" mean, precisely? What are the requirements that it fails? $\endgroup$
    – D.W.
    Commented Nov 15 at 8:07

3 Answers 3

6
$\begingroup$

You can do this with dynamic programming. Let $S$ be the input set, and let $N=\max S$. We define the table $T\in\mathbb{N}^N$ where $$ T[w] = \min_{\substack{X\subseteq S\\ \operatorname{gcd}(A[X])=w}} \sum\limits_{x\in X}B[x]. $$ Clearly, your goal is to compute $T[1]$.

This table can be computed by the following recursive formula $$ T[w] = \min_{\substack{x\in S, w' \in [w+1, N]\\ \operatorname{gcd}(A[x], w') = w}} T[w']+B[x]. $$ It is not hard to see that this formula computes the table $T$ correctly, and a naive implementation has running time $O(N^2\cdot|S|)$.

However, you can get a much better running time with the following observation: In order to compute $T[w]$ you do not need to iterate over all values $w'$ greater than $w$, but only on multiples of $w$, since non-multiples won't have $\operatorname{gcd}$ equal to $w$. This has running time $O(N\log N \cdot |S|)$, since the number of outer iterations can be written as $\sum_{i=1}^n \frac{n}{i} = n \cdot H_n$ where $H_n$ is the Haromnic series of order $n$ and is of order $\log n$.

One final notice: since each pair $w'$, $x$ only update one index $T[w]$, it suffices to iterate over all values $w'$ in decreasing order, and for each, to iterate over all values $x\in S$, and compute $w = \operatorname{gcd}(A[x], w')$ and update $T[w]$ accordingly. This has running time $O(N\cdot |S|)$.

$\endgroup$
3
  • $\begingroup$ This is pseudo-polynomial, though - polynomial in the input values, rather than the input size. $\endgroup$ Commented Nov 14 at 18:38
  • $\begingroup$ True, my algorithm is not polynomial when weights are given in binary, but only in unary. The problem is quite close to the Knapsack problem as well. I just restricted to the given constraints (small values, so pseudoplynomial is enough) $\endgroup$ Commented Nov 15 at 14:17
  • 1
    $\begingroup$ I think your approach is correct I got AC for this problem with this approach, Thanks bro. $\endgroup$
    – Zaak
    Commented Nov 24 at 19:58
3
$\begingroup$

This is NP-hard. We will prove this by reduction from the set cover problem.

Consider a set $U$, and a set $S$ of subsets of $U$. The set cover problem asks for the smallest subset of $S$ whose union is $U$. To transform this problem into an instance of your problem, we start by taking the set $P$ of the first $|U|$ prime numbers, and pick a bijection $f$ from $U$ to $P$.

Then we associate each $s$ in $S$ with

$$\prod_{x \in U \setminus s}f(x)$$

the product of all primes in $P$ not associated with elements of $s$.

Now we take $A$ to be an array of all these products, and $B$ to be an array of ones. A subset of $A$ has GCD $1$ if and only if it corresponds to a subset of $S$ that covers $U$, and the cheapest subset of $A$ corresponds to the smallest subset of $S$ that covers $U$.

(The algorithm in Narek's answer does not solve this problem in polynomial time - its runtime is pseudo-polynomial, polynomial in the input values rather than the input size.)

$\endgroup$
3
  • $\begingroup$ I think mapping and computing prime numbers have an issue. There is a theorem about distance between 2 consecutive prime numbers, i.e. $p_2-p_1$, that all such distance are possible, so I worry when $|U|$ is large enough, $|U|$th primes is larger than $2^{2^{|U|}}$, implying that the reduction is not polynomial. $\endgroup$ Commented Nov 14 at 19:42
  • 2
    $\begingroup$ @minhquýlê: Primes are too common for that to be a problem - the nth prime is around $n \log(n)$. $\endgroup$ Commented Nov 14 at 19:53
  • $\begingroup$ True, my algorithm is not polynomial when weights are given in binary. It is quite close to the Knapsack problem as well, I just restricted to the given constraints (small values, so pseudoplynomial is enough) $\endgroup$ Commented Nov 15 at 14:14
0
$\begingroup$

State Definition: We use dp[c][gcd] represents the minimal total B[i] to achieve GCD gcd with c elements.

Transfer function: $$ dp[c][\text{new_gcd}] = \min(dp[c][\text{new_gcd}], \quad dp[c-1][\text{existing_gcd}] + B[i]). \\ \text{ where } \text{new_gcd}=\operatorname{GCD}(\text{existing_gcd}, A[i]) $$

Initial condition: $ dp[0][0]=0 $ since when choosing 0 elements, the GCD is 0, and the total cost is 0.

The desired result: it is stored in dp[k][1], representing the minimum cost to select k elements whose GCD is 1. If dp[k][1] is not defined, it means it's impossible to select k elements with GCD 1.

from dataclasses import dataclass
from functools import reduce
from itertools import combinations
from math import gcd
from time import time
from typing import List, Tuple, Optional
import concurrent.futures
import math
import random


@dataclass
class TestCase:
    test_num: int
    n: int
    k: int
    A: List[int]
    B: List[int]


@dataclass
class TestResult:
    test_num: int
    n: int
    k: int
    A: List[int]
    B: List[int]
    brute_force_result: Optional[int]
    brute_force_time: float
    dp_result: int
    dp_time: float
    indices: Optional[List[int]]
    is_valid: bool
    solutions_match: bool


def find_gcd_array(numbers):
    """Calculate GCD of an array of numbers."""
    return reduce(gcd, numbers)


def find_min_sum_with_gcd_one(A, B, k):
    """
    Find minimum sum of k elements from array B where corresponding elements
    from A have GCD = 1
    """
    n = len(A)
    min_sum = float('inf')
    chosen_indices = None

    for indices in combinations(range(n), k):
        selected_A = [A[i] for i in indices]
        current_gcd = find_gcd_array(selected_A)

        if current_gcd == 1:
            current_sum = sum(B[i] for i in indices)
            if current_sum < min_sum:
                min_sum = current_sum
                chosen_indices = list(indices)

    if min_sum == float('inf'):
        return None, None

    return min_sum, chosen_indices


def min_total_b_dp(n, k, A, B):
    """Dynamic programming solution."""
    dp = [dict() for _ in range(k + 1)]
    dp[0][0] = 0

    for i in range(n):
        Ai = A[i]
        Bi = B[i]
        new_dp = [dict(dp_c) for dp_c in dp]
        for c in range(k, 0, -1):
            for g in dp[c - 1]:
                new_gcd = math.gcd(g, Ai)
                new_cost = dp[c - 1][g] + Bi
                if new_gcd not in new_dp[c] or new_dp[c][new_gcd] > new_cost:
                    new_dp[c][new_gcd] = new_cost
        dp = new_dp

    return dp[k].get(1, -1)


def generate_test_case(n_min=5, n_max=20, k_min=2, val_max=100) -> TestCase:
    """Generate a random test case."""
    n = random.randint(n_min, n_max)
    k = random.randint(k_min, n)

    # Generate A array with some numbers that ensure GCD=1 is possible
    A = [random.randint(1, val_max) for _ in range(n)]
    # Make sure at least k numbers are coprime
    coprime_numbers = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
    for i in range(min(k, len(coprime_numbers))):
        A[i] = coprime_numbers[i]
    random.shuffle(A)

    # Generate B array
    B = [random.randint(1, val_max) for _ in range(n)]

    return TestCase(0, n, k, A, B)  # test_num will be set later


def verify_solution(n, k, A, B, min_sum, indices):
    """Verify that a solution is valid."""
    if min_sum is None or indices is None:
        return False

    if len(indices) != k:
        return False

    if any(i >= n for i in indices):
        return False

    selected_A = [A[i] for i in indices]
    if find_gcd_array(selected_A) != 1:
        return False

    selected_B = [B[i] for i in indices]
    if sum(selected_B) != min_sum:
        return False

    return True


def run_single_test(test_case: TestCase) -> TestResult:
    """Run a single test case and return the results."""
    start_time = time()
    brute_force_result, indices = find_min_sum_with_gcd_one(test_case.A, test_case.B, test_case.k)
    brute_force_time = time() - start_time

    start_time = time()
    dp_result = min_total_b_dp(test_case.n, test_case.k, test_case.A, test_case.B)
    dp_time = time() - start_time

    # Verify results
    if brute_force_result is not None:
        brute_force_valid = verify_solution(test_case.n, test_case.k, test_case.A,
                                            test_case.B, brute_force_result, indices)
    else:
        brute_force_valid = (dp_result == -1)

    dp_matches = (dp_result == brute_force_result) or (dp_result == -1 and brute_force_result is None)

    return TestResult(
        test_num=test_case.test_num,
        n=test_case.n,
        k=test_case.k,
        A=test_case.A,
        B=test_case.B,
        brute_force_result=brute_force_result,
        brute_force_time=brute_force_time,
        dp_result=dp_result,
        dp_time=dp_time,
        indices=indices,
        is_valid=brute_force_valid,
        solutions_match=dp_matches
    )


def print_test_result(result: TestResult):
    """Print the results of a single test."""
    print(f"\nTest {result.test_num + 1}:")
    print(f"n={result.n}, k={result.k}")
    print(f"A={result.A}")
    print(f"B={result.B}")
    print(f"Brute Force Result: {result.brute_force_result} (Time: {result.brute_force_time:.4f}s)")
    print(f"DP Result: {result.dp_result} (Time: {result.dp_time:.4f}s)")
    print(f"Solutions match: {result.solutions_match}")
    print(f"Brute force solution valid: {result.is_valid}")


def run_tests_concurrent(num_tests=100, max_workers=None):
    """Run multiple random tests concurrently and compare both implementations."""
    print(f"Running {num_tests} random tests concurrently...")

    # Generate all test cases first
    test_cases = [generate_test_case() for _ in range(num_tests)]
    for i, test_case in enumerate(test_cases):
        test_case.test_num = i

    # Run tests concurrently
    start_time = time()
    with concurrent.futures.ProcessPoolExecutor(max_workers=max_workers) as executor:
        future_to_test = {executor.submit(run_single_test, test_case): test_case
                          for test_case in test_cases}

        results = []
        for future in concurrent.futures.as_completed(future_to_test):
            test_case = future_to_test[future]
            try:
                result = future.result()
                results.append(result)
                print_test_result(result)

                if not result.solutions_match or not result.is_valid:
                    print("ERROR: Solutions don't match or invalid solution found!")
                    return False

            except Exception as e:
                print(f"Test {test_case.test_num} generated an exception: {e}")
                return False

    total_time = time() - start_time

    # Calculate and print statistics
    avg_brute_force_time = sum(r.brute_force_time for r in results) / len(results)
    avg_dp_time = sum(r.dp_time for r in results) / len(results)

    print("\nTest Summary:")
    print(f"Total tests: {num_tests}")
    print(f"Total time: {total_time:.2f}s")
    print(f"Average brute force time: {avg_brute_force_time:.4f}s")
    print(f"Average DP time: {avg_dp_time:.4f}s")
    print("All tests passed successfully!")

    return True


def main():
    # First test the example case
    print("Testing example case:")
    n, k = 5, 4
    A = [10, 2, 5, 7, 8]
    B = [7, 9, 11, 2, 3]

    min_sum, indices = find_min_sum_with_gcd_one(A, B, k)
    dp_result = min_total_b_dp(n, k, A, B)

    print(f"Brute Force - Minimum sum: {min_sum}")
    print(f"Brute Force - Chosen indices: {indices}")
    print(f"DP Solution Result: {dp_result}")

    # Then run concurrent random tests
    run_tests_concurrent(1000)


if __name__ == "__main__":
    main()
$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.