2.14 Count Ways To Pick K Coins Divisible By M
Source:
src/main/kotlin/google/CountNumberOfWaysToPickKCoinsSumDivisibleByM.ktPattern: memoized (index, count, remainder) · Core page
The Problem
Coins are numbered 0..n-1. Count how many ways to pick exactly k coins such that their sum is divisible by m (result mod $10^9+7$).
- Constraints: $1 \le k \le n$;
mfits inInt.
Examples
Input: n = 4, k = 2, m = 3 -> Output: 2 ({0,3} and {1,2} both sum to a multiple of 3)
Input: n = 5, k = 3, m = 3 -> Output: 2
Intuition — the state is (index, picks left, remainder); the remainder is the carry
The count-with-a-condition DP needs three axes:
idx— which coin we’re deciding next;k— how many picks remain;rem— the running sum modulo m (the only part of the sum that matters for divisibility).
The recurrence is the classic pick/skip:
solve(idx, k, rem):
k == 0 -> 1 iff rem == 0
(n - idx) < k -> 0 (not enough coins left — pruning)
else -> solve(idx+1, k, rem) # skip coin idx
+ solve(idx+1, k-1, (rem + idx) % m) # pick coin idx
Why does rem carry modulo instead of the raw sum? Only sum % m decides divisibility, and (a + b) % m is computable from a % m — so the remainder is a complete summary of the sum, bounded by m instead of by n·m. That’s what keeps the state space at $O(n \cdot k \cdot m)$ rather than exponential.
Why the (n - idx) < k pruning? If fewer coins remain than picks needed, no completion exists — the branch dies without recursion. The same “remaining resources vs remaining needs” cut as 11.2’s reachability frontier, in DP clothing.
The coins are 0-indexed (the repo’s comment: coins = [0, 1, 2, 3]), so picking coin idx adds idx to the sum — (rem + idx) % m. Careful: not idx + 1.
Approach 1 — Enumerate all C(n, k) combinations (exponential)
Generate every k-subset and check the sum: correct, dies at n = 20.
Approach 2 — Memoized (idx, k, rem) (the repo’s version, optimal)
fun countWays(n: Int, k: Int, m: Int): Int {
val mod = 1_000_000_007
data class State(val idx: Int, val k: Int, val rem: Int)
val _cache = mutableMapOf<State, Int>()
fun solve(idx: Int, k: Int, rem: Int): Int =
_cache.getOrPut(State(idx, k, rem)) {
when {
k == 0 -> if (rem == 0) 1 else 0
// Pruning: if coins remaining (n - idx) < coins needed (k), stop
(n - idx) < k || idx == n -> 0
else -> {
val skip = solve(idx + 1, k, rem)
val pick = solve(idx + 1, k - 1, (rem + (idx % m)) % m)
(skip + pick) % mod
}
}
}
return solve(0, k, 0)
}
import java.util.*;
public class CountWaysToPickKCoinsDivisibleByM {
private static final int MOD = 1_000_000_007;
/**
* @param n number of coins (0..n-1)
* @param k coins to pick
* @param m divisor
* @return ways to pick k coins with sum divisible by m
*/
public int countWays(int n, int k, int m) {
Map<String, Integer> memo = new HashMap<>();
return solve(0, k, 0, n, m, memo);
}
private int solve(int idx, int k, int rem, int n, int m, Map<String, Integer> memo) {
if (k == 0) return rem == 0 ? 1 : 0;
if (n - idx < k || idx == n) return 0;
String key = idx + "," + k + "," + rem;
if (memo.containsKey(key)) return memo.get(key);
int skip = solve(idx + 1, k, rem, n, m, memo);
int pick = solve(idx + 1, k - 1, (rem + idx) % m, n, m, memo);
int result = (skip + pick) % MOD;
memo.put(key, result);
return result;
}
}
#include <cstring>
class CountWaysToPickKCoinsDivisibleByM {
long long memo[31][31][31];
int n, m, k;
const long long MOD = 1'000'000'007LL;
long long solve(int idx, int left, int rem) {
if (left == 0) return rem == 0 ? 1 : 0;
if (n - idx < left || idx == n) return 0;
if (memo[idx][left][rem] != -1) return memo[idx][left][rem];
long long skip = solve(idx + 1, left, rem);
long long pick = solve(idx + 1, left - 1, (rem + idx) % m);
return memo[idx][left][rem] = (skip + pick) % MOD;
}
public:
/**
* @param n number of coins (0..n-1)
* @param k coins to pick
* @param m divisor
* @return ways to pick k coins with sum divisible by m
*/
int countWays(int n, int k, int m) {
this->n = n; this->k = k; this->m = m;
std::memset(memo, -1, sizeof memo);
return (int)solve(0, k, 0);
}
};
from functools import lru_cache
MOD = 1_000_000_007
def count_ways(n: int, k: int, m: int) -> int:
"""
@param n: number of coins (0..n-1)
@param k: coins to pick
@param m: divisor
@return: ways to pick k coins with sum divisible by m
"""
@lru_cache(None)
def solve(idx: int, left: int, rem: int) -> int:
if left == 0:
return 1 if rem == 0 else 0
if n - idx < left or idx == n:
return 0 # not enough coins left — pruning
skip = solve(idx + 1, left, rem)
pick = solve(idx + 1, left - 1, (rem + idx) % m)
return (skip + pick) % MOD
return solve(0, k, 0)
#![allow(unused)]
fn main() {
use std::collections::HashMap;
impl Solution {
/// @param n number of coins (0..n-1)
/// @param k coins to pick
/// @param m divisor
/// @return ways to pick k coins with sum divisible by m
pub fn count_ways(n: i32, k: i32, m: i32) -> i32 {
const MOD: i64 = 1_000_000_007;
let mut memo: HashMap<(i32, i32, i32), i64> = HashMap::new();
fn solve(idx: i32, left: i32, rem: i32, n: i32, m: i32,
memo: &mut HashMap<(i32, i32, i32), i64>) -> i64 {
if left == 0 { return if rem == 0 { 1 } else { 0 }; }
if n - idx < left || idx == n { return 0; } // pruning
if let Some(&v) = memo.get(&(idx, left, rem)) { return v; }
let skip = solve(idx + 1, left, rem, n, m, memo);
let pick = solve(idx + 1, left - 1, (rem + idx) % m, n, m, memo);
let v = (skip + pick) % MOD;
memo.insert((idx, left, rem), v);
v
}
solve(0, k, 0, n, m, &mut memo) as i32
}
}
}
Dry run
Input: n = 4, k = 2, m = 3 — coins {0,1,2,3}, pick 2 with sum % 3 == 0.
enumerate (idx strictly increasing): pairs {0,3} -> 0+3=3 ✓, {1,2} -> 3 ✓.
All other pairs: 0+1=1, 0+2=2, 1+3=4≡1, 2+3=5≡2 -> fail. Answer: 2.
DP path (abridged): solve(0,2,0)
skip -> solve(1,2,0): eventually counts pairs among {1,2,3}: {1,2} ✓ -> 1
pick -> solve(1,1,0%3=0): counts pairs starting with coin 0:
pick coin 1 -> solve(2,0,1): rem 1 != 0 -> 0
pick coin 2 -> solve(3,0,2): 0
pick coin 3 -> solve(4,0,3%3=0): 1 -> the {0,3} pair ✓
total = 1 + 1 = 2 ✓
The remainder carry in action: picking coin 3 adds 3 % 3 = 0, so the state (4, 0, 0) closes the {0,3} choice — the raw sum never appears, only its residue. The pruning (n - idx) < left kills branches like “pick 2 coins from only 1 remaining” instantly.
Complexity
Time. States n × k × m, O(1) per state:
$$ T(n, k, m) = O(n \cdot k \cdot m) $$
Space. The memo:
$$ S(n, k, m) = O(n \cdot k \cdot m) $$
Variants & follow-ups
- Target Sum (
array/dp/TargetSum.kt) — the same (index, remainder-carry) counting, with a signed target instead of a modulo. - Partition Equal Subset Sum (2.6) — divisibility reachability without the pick-count axis.
- Interview follow-up: “Why does
remmake the state small?” Onlysum % mdetermines divisibility, and it composes under addition — so the remainder is a lossless summary of the sum, bounded bym(≤ 30 here). Replace the remainder with the raw sum and the state space explodes to $n \cdot k \cdot (n \cdot m)$.