Chixiyu's blog

By Chixiyu, history, 2 days ago, In English

CF: Mortal Combat Tower

1418C - Mortal Kombat Tower Problem — 1418C — Codeforces

Simple problem.... but solution 1 took me a long time to debug, mainly because there was a sneaky hidden bug

Solution 1: DP

Simple brute force memoization search of all cases:

State: $$$dp[i][j]$$$ means at position $$$j$$$ when switched to player $$$i\in[0,1]$$$ (0=my friend, 1=me), the minimum skip points used. So the answer is $$$min(dp[0][n],dp[1][n])$$$, checking at the end which player's side has the minimum skip points.

Initial state: $$$dp[1][0]=0$$$ means if switched to me at position 0 (although impossible since friend starts first), then 0 skip points were used.

Code:

#include <bits/stdc++.h>
using namespace std;

int main() {
    int test_case_num;
    cin >> test_case_num;

    for (int t = 0; t < test_case_num; t++) {
        int n;
        cin >> n;

        vector<int> bosses(n);
        for (int j = 0; j < n; j++) {
            cin >> bosses[j];
        }

        /*
         * dp[i][j] = the min amount of skip points
         * on turn i (your turn is 0), on the jth boss.
         * (1e9 to prevent overflow)
         */
        vector<vector<int>> dp(2, vector<int>(n + 1, 1e9));

        /*
         * base case:
         * your friend uses zero skip points before fighting any bosses.
         */
        dp[1][0] = 0;

        for (int j = 0; j < n; j++) {
            // the opposite player switches on the previous move.
            dp[0][j + 1] = min(dp[0][j + 1], dp[1][j] + bosses[j]);
            dp[1][j + 1] = min(dp[1][j + 1], dp[0][j]);

            // the opposite player switches from the previous two moves.
            if (j + 2 <= n) {
                dp[0][j + 2] =
                    min(dp[0][j + 2], dp[1][j] + bosses[j] + bosses[j + 1]);
                dp[1][j + 2] = min(dp[1][j + 2], dp[0][j]);
            }
        }
        cout << min(dp[0][n], dp[1][n]) << endl;
    }
}

The annoying part is, since we need to find min, the array should be initialized to infinity right? But the bug is that if I set it to INT_MAX, during the DP process it will increase above that, causing integer overflow to INT_MIN, and since we're always taking min, it gets carried to the end outputting INT_MIN. I spent so long debugging without realizing this sneaky issue...

Setting it smaller to 1e9 works fine, as it won't cause integer overflow.

Solution 2: Greedy

Friend must go first, so use skip points on first position if needed. The key is how to choose switches after that.

1 1 1 1 1 1

Consider a consecutive sequence of 1s, the optimal case should be me taking two 1s, switch (must switch) to friend using one skip point, then let me take two more...

We can observe the conclusion (quite difficult, I also saw it from the editorial): For a subsequence of $$$k$$$ consecutive 1s, minimum skip points needed is $$$\lfloor \frac{k}{3} \rfloor$$$. So, just count lengths of consecutive 1 sequences, calculate skip points used, done.

Code (not original, from Educational Codeforces Round 95 Editorial — Codeforces):

#include <bits/stdc++.h>

using namespace std;

int main() {
#ifdef _DEBUG
    freopen("input.txt", "r", stdin);
//  freopen("output.txt", "w", stdout);
#endif
    
    int t;
    cin >> t;
    while (t--) {
        int n;
        cin >> n;
        vector<int> a(n);
        for (auto &it : a) cin >> it;
        int ans = 0;
        ans += a[0] == 1;
        for (int i = 1; i < n; ++i) {
            if (a[i] == 0) {
                continue;
            }
            int j = i;
            while (j < n && a[j] == 1) {
                ++j;
            }
            ans += (j - i) / 3;
            i = j - 1;
        }
        cout << ans << endl;
    }
    
    return 0;
}

Full text and comments »

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

By Chixiyu, history, 2 days ago, In English

Point Update Range Sum

(translated from Chinese using GPT4o) References:

Introduction:

Given an array $$$a$$$, and two integers $$$x, y$$$, perform the following operations:

  1. Compute the sum of $$$a[x...y]$$$.
  2. Increment $$$a[x]$$$ by $$$y$$$.

The first operation is called a range sum query, and the second is a point update. While prefix sums suffice for just range sum queries, they fail when updates are involved. For this, we need more advanced data structures to handle both range queries and updates efficiently.


Fenwick Tree (Binary Indexed Tree)

The Fenwick Tree appears first. It may be harder to understand than a segment tree, but its implementation is simpler.

The following diagram illustrates a Fenwick Tree (from OI Wiki):

Properties

Here, $$$a$$$ is the original array, and $$$c$$$ is the Fenwick Tree. The elements of $$$c$$$ store the sums of certain segments of $$$a$$$. But how do we determine which segment $$$a[x...y]$$$ corresponds to $$$c[i]$$$?

First, we observe that $$$y$$$ (the rightmost endpoint of the segment associated with $$$c[i]$$$) is always equal to $$$i$$$. For example, $$$c[4] = \sum a[1...4]$$$. Thus, the key is determining $$$x$$$, the leftmost endpoint.

From the diagram, we observe the following patterns:

  • Level 1: Indices $$$1, 3, 5, 7$$$ (all odd numbers).
  • Level 2: Indices $$$2, 6$$$.
  • Level 3: Index $$$4$$$.
  • Level 4: Index $$$8$$$.

Each level represents the range length it covers. For example, at Level 3, $$$c[4]$$$ covers $$$a[1...4]$$$. So, at Level $$$n$$$, $$$c[i]$$$ covers $$$a[i-n+1...i]$$$. Now the problem reduces to determining the level of $$$c[i]$$$.

We introduce binary representation to solve this problem. Let’s summarize:

  • Level 1:
  • $$$(1)_2 = 1$$$
  • $$$(3)_2 = 11$$$
  • $$$(5)_2 = 101$$$
  • $$$(7)_2 = 111$$$
  • All end in $$$1$$$.
  • Level 2:
  • $$$(2)_2 = 10$$$
  • $$$(6)_2 = 1010$$$
  • All end in $$$10$$$.
  • Level 3:
  • $$$(4)_2 = 100$$$
  • All end in $$$100$$$.
  • Level 4:
  • $$$(8)_2 = 1000$$$
  • All end in $$$1000$$$.

Thus, the level of $$$c[i]$$$ is determined by the binary representation of $$$i$$$, specifically the lowest set bit and all trailing zeros. This can be computed using lowbit.

Lowbit

lowbit(x) extracts the lowest set bit of $$$x$$$ and all trailing zeros. Using bit manipulation, lowbit(x) = x & -x. Here’s the code:

int lowbit(int x) {
  // Extracts the lowest bit and trailing 0s.
  return x & -x;
}

Note: lowbit does not return the position of the last set bit but the number represented by $$$1000...$$$ in binary.

Thus, $$$c[x] = \sum_{i=x-\text{lowbit}(x)+1}^x a[i]$$$. This is the sum stored in each Fenwick Tree element.

Query

Given $$$x, y$$$, compute the sum of $$$a[x...y]$$$. For a query like $$$a[1...8]$$$, the answer is simply $$$c[8]$$$. For $$$a[2...8]$$$, the range is split into $$$c[8] - a[1]$$$. Similarly, for $$$a[2...7]$$$, using prefix sums:
$$$a[2...7] = \text{prefix}[7] - \text{prefix}[1]$$$.

Thus, we only need a function to compute $$$a[1...x]$$$ and subtract extra parts, similar to prefix sums.

For $$$a[1...y]$$$, start at i = y and repeatedly jump left (i = i - lowbit(i)) until i = 0.

Code:

int sum(int l, int r) {
    int i = r;
    int acc = 0;
    for (; i > 0; i = i - lowbit(i)) {
        acc += f[i];
    }
}
Query Time Complexity Proof

(From OI Wiki) Consider the binary representation of $$$r$$$ and $$$l$$$. Their leftmost differing bit (from highest to lowest) determines jumps in the Fenwick Tree. Each jump reduces the problem size logarithmically. Hence, the total complexity is $$$\Theta(\log^2 n)$$$.

Point Update

Point updates reverse the process of range queries. To increment $$$a[x]$$$ by $$$k$$$, update all $$$c[i]$$$ that "cover" $$$a[x]$$$. These are the indices $$$c[x], c[x+\text{lowbit}(x)], c[x+2\times \text{lowbit}(x)], \dots$$$.

Code:

void update(int x, int k) {
    for (int i = x; i <= n; i += lowbit(i)) {
        c[i] += k;
    }
}
Update Time Complexity

During updates, the height of visited nodes strictly increases. For a Fenwick Tree of size $$$n$$$, this height is bounded by $$$\log n$$$. Hence, the complexity is $$$\Theta(\log n)$$$.

Build Tree

Building the tree involves $$$n$$$ point updates, resulting in $$$O(n\log n)$$$ complexity.


Complete Template

template <typename T>
class FenwickTree {
private:
    vector<T> c;
    int n;

    int lowbit(int x) { return x & -x; }

public:
    FenwickTree(int size) {
        n = size;
        c.resize(n + 1, 0);
    }

    void update(int x, T k) {
        for (int i = x; i <= n; i += lowbit(i)) {
            c[i] += k;
        }
    }

    T query(int x) {
        T sum = 0;
        for (int i = x; i > 0; i -= lowbit(i)) {
            sum += c[i];
        }
        return sum;
    }

    T rangeQuery(int l, int r) {
        return query(r) - query(l - 1);
    }

    void build(const vector<T>& a) {
        for (int i = 1; i <= n; ++i) {
            update(i, a[i - 1]);
        }
    }
};
Example Usage:
int main() {
    vector<int> a = {1, 2, 3, 4, 5};
    int n = a.size();

    FenwickTree<int> fenwick(n);
    fenwick.build(a);
    cout << "Range [2, 4]: " << fenwick.rangeQuery(2, 4) << endl; // Output: 9

    fenwick.update(3, 5);
    cout << "After update, range [2, 4]: " << fenwick.rangeQuery(2, 4) << endl; // Output: 14
    return 0;
}
Problem Example:

P3374 Luogu

#include <bits/stdc++.h>
using namespace std;

template <typename T>
class FenwickTree {
    vector<T> c;
    int n;

    int lowbit(int x) { return x & -x; }

public:
    FenwickTree(int size) {
        n = size;
        c.resize(n + 1, 0);
    }

    void update(int x, T k) {
        for (int i = x; i <= n; i += lowbit(i)) {
            c[i] += k;
        }
    }

    T query(int x) {
        T sum = 0;
        for (int i = x; i > 0; i -= lowbit(i)) {
            sum += c[i];
        }
        return sum;
    }

    T rangeQuery(int l, int r) { return query(r) - query(l - 1); }

    void build(const vector<T>& a) {
        for (int i = 1; i <= n; ++i) {
            update(i, a[i - 1]);
        }
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, m;
    cin >> n >> m;
    vector<int> a(n);
    for (auto& it : a) cin >> it;
    FenwickTree<int> ft(n);
    ft.build(a);
    while (m--) {
        int op;
        cin >> op;
        if (op == 1) {
            int x, k;
            cin >> x >> k;
            ft.update(x, k);
        } else {
            int l, r;
            cin >> l >> r;
            cout << ft.rangeQuery(l, r) << endl;
        }
    }
    return 0;
}

Full text and comments »

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

By Chixiyu, history, 2 days ago, In English

CF2064C: Remove the Ends

Problem — C — Codeforces 2064C - Remove the Ends

Approach 1 (TLE): Memoized Search

Direct memoized search using unordered_map. We need to combine l and r states into a single long long for storage. Unfortunately, TLEs on test case 5.

// Note: TLE
#include <bits/stdc++.h>
using namespace std;
static inline long long encodeState(int l, int r) {
    return ((long long)l << 32) | (unsigned int)r;
}

unordered_map<long long, long long> dpMemo;

vector<int> a;

long long dp(int l, int r) {
    if (l > r)
        return 0;
    long long key = encodeState(l, r);
    if (dpMemo.count(key))
        return dpMemo[key];
    long long best = -LLONG_MAX / 2;
    for (int i = l; i <= r; i++) {
        if (a[i] > 0) {
            best = max(best, (long long)a[i] + dp(i + 1, r));
        } else if (a[i] < 0) {
            best = max(best, (long long)(-a[i]) + dp(l, i - 1));
        }
    }
    dpMemo[key] = best;
    return best;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int t;
    cin >> t;
    while (t--) {
        int n;
        cin >> n;
        a.resize(n + 1);
        for (int i = 1; i <= n; i++) {
            cin >> a[i];
        }
        dpMemo.clear();
        long long ans = dp(1, n);
        cout << ans << "\n";
    }
    return 0;
}

Approach 2 (AC): Left-Right DP Merge

Let's look for patterns: - If array a is all positive, maximize by taking left to right: $$$ans=\sum^n_{i=0}a_i$$$ - If all negative, take right to left - Or split: take [0,i] from left and [i+1,n] from right

We can use two DP arrays: p_dp (prefix) and s_dp (suffix). $$$p_dp[i]$$$ represents maximum answer taking positives from left up to position i:

$$$ j=max(p\_dp[0...i-1])\\ p\_dp[i]=max(p\_dp[i],p\_dp[j]+a[i]), a[i]>0\\ a[i]=a[n-i+1],i=[1,n]\\ s\_dp[i]=max(s\_dp[i],s\_dp[j]-a[i]), a[i]<0\\ $$$

Important constraints:

$$$ p\_dp[i]=NEGINF,a[i]<0\\ s\_dp[i]=NEGINF,a[i]>0 $$$

The third way combines prefix and suffix:

$$$ combined\_ans=max(combined\_ans,p\_dp[i]+s\_dp[n-i]) $$$

Final answer is maximum of all three approaches:

$$$ ans=max(p\_dp[n],s\_dp[n],combined\_ans) $$$

Code:

/*
 * Created by: Chixiyu
 * Date: 2025-02-17
 */
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const ll NEG_INF = -1000000000000000000LL; // -1e18

void solve() {
    int n;
    cin >> n;
    vector<int> a(n + 1);
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    // prefix
    vector<ll> prefix_dp(n + 1, NEG_INF);
    ll prefix_ans;
    ll prev_max = 0; // prevmax= max(a[0,i-1])
    prefix_dp[0] = 0;
    for (int i = 1; i <= n; i++) {
        if (a[i] > 0) { // can prefix dp1
            prefix_dp[i] = max(prefix_dp[i], a[i] + prev_max);
            // update prev_max
            prev_max = max(prev_max, prefix_dp[i]);
        }
    }
    prefix_ans = prefix_dp[n];
    // suffix
    vector<ll> suffix_dp(n + 1, NEG_INF);
    ll suffix_ans;
    prev_max = 0;
    // inverse the array
    reverse(++a.begin(), a.end());
    suffix_dp[0] = 0;
    for (int i = 1; i <= n; i++) {
        if (a[i] < 0) { // can suffix dp
            suffix_dp[i] = max(suffix_dp[i], -a[i] + prev_max);
            prev_max = max(prev_max, suffix_dp[i]);
        }
    }
    suffix_ans = suffix_dp[n];
    // calculate combined
    ll combined_ans = NEG_INF;
    for (int i = 0; i <= n; i++) {
        ll left = prefix_dp[i];
        ll right = 0;
        if (i != n) {
            right = suffix_dp[n - i];
        }
        combined_ans = max(combined_ans, left + right);
    }
    cout << max({prefix_ans, suffix_ans, combined_ans}) << endl;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int t;
    cin >> t;
    while (t--) {
        solve();
    }
    return 0;
}

Full text and comments »

Tags dp
  • Vote: I like it
  • -1
  • Vote: I do not like it