Skip to main content

atcoder/lib/
floor_sum.rs

1//! 床関数の和に関するアルゴリズムです。
2
3/// 等差数列の各項を `m` で割った商(床)の総和を返します。
4///
5/// 次の値を `O(log m)` で計算します。
6///
7/// ```text
8/// sum_{i = 0}^{n - 1} floor((a * i + b) / m)
9/// ```
10///
11/// `n` は項数(和の対象は `i = 0..n` の半開区間)、`m` は除数、`a` は係数、`b` は定数項です。
12/// すべての引数は `u64` なので非負であり、`m` は正でなければなりません。
13///
14/// 計算量は `O(log m)` です。戻り値は `u128` ですが、非常に大きな入力では総和が `u128` に
15/// 収まらないことがあります。正しい結果を得るには総和が `u128` に収まり、計算途中の値も
16/// `u128` の範囲内であることが必要です。積の計算には `u128` を使います。
17///
18/// # Panics
19///
20/// `m` が `0` の場合は panic します。
21///
22/// # Examples
23///
24/// ```
25/// use atcoder::floor_sum::floor_sum;
26///
27/// let sum = floor_sum(6, 5, 3, 2);
28///
29/// // floor(2 / 5) + floor(5 / 5) + floor(8 / 5)
30/// // + floor(11 / 5) + floor(14 / 5) + floor(17 / 5)
31/// // = 0 + 1 + 1 + 2 + 2 + 3
32/// assert_eq!(sum, 9);
33/// ```
34pub fn floor_sum(mut n: u64, mut m: u64, mut a: u64, mut b: u64) -> u128 {
35    assert!(m > 0, "modulus must be positive");
36    if n == 0 {
37        return 0;
38    }
39
40    let mut sum = 0u128;
41
42    loop {
43        // a = q * m + r と分解する。
44        //
45        // floor((a * i + b) / m)
46        // = q * i + floor((r * i + b) / m)
47        //
48        // であるため、q * (0 + 1 + ... + n - 1) を先に加える。
49        if a >= m {
50            let quotient = a / m;
51
52            sum += n as u128 * (n - 1) as u128 / 2 * quotient as u128;
53
54            a %= m;
55        }
56
57        // b = q * m + r と分解する。
58        //
59        // 各項に q が含まれるため、q * n を先に加える。
60        if b >= m {
61            let quotient = b / m;
62
63            sum += n as u128 * quotient as u128;
64            b %= m;
65        }
66
67        // 半開区間の右端 i = n における一次式の値を求める。
68        let y_max = a as u128 * n as u128 + b as u128;
69
70        // a * n + b < m なら、残りの各項の商はすべて 0 になる。
71        if y_max < m as u128 {
72            break;
73        }
74
75        // 直線の下にある格子点を、列ごとではなく行ごとに数え直す。
76        //
77        // floor_sum(n, m, a, b)
78        // = floor_sum(
79        //     floor((a * n + b) / m),
80        //     a,
81        //     m,
82        //     (a * n + b) mod m,
83        //   )
84        //
85        // という変形を行う。
86        n = (y_max / m as u128) as u64;
87        b = (y_max % m as u128) as u64;
88
89        std::mem::swap(&mut m, &mut a);
90    }
91
92    sum
93}
94
95#[cfg(test)]
96mod tests {
97    use super::floor_sum;
98
99    /// 定義どおりに `O(n)` で床関数の和を計算します。
100    fn floor_sum_naive(n: u64, m: u64, a: u64, b: u64) -> u128 {
101        (0..n)
102            .map(|i| (a as u128 * i as u128 + b as u128) / m as u128)
103            .sum()
104    }
105
106    #[test]
107    fn calculates_floor_sum() {
108        assert_eq!(floor_sum(6, 5, 3, 2), 9);
109        assert_eq!(floor_sum(0, 5, 8, 7), 0);
110        assert_eq!(floor_sum(10, 7, 0, 15), 20);
111    }
112
113    #[test]
114    fn matches_naive_calculation() {
115        for n in 0..20 {
116            for m in 1..20 {
117                for a in 0..20 {
118                    for b in 0..20 {
119                        assert_eq!(
120                            floor_sum(n, m, a, b),
121                            floor_sum_naive(n, m, a, b),
122                            "n = {n}, m = {m}, a = {a}, b = {b}",
123                        );
124                    }
125                }
126            }
127        }
128    }
129}