NeFut Logo NeFut
Admin Login

Breaking the Limits of State Compression: The Application of Ternary and Minimal Representation

Published at: 2026-05-27 09:17 Last updated: 2026-06-06 13:04
#algorithm #optimization #Dynamic Programming

Core Logic and Mathematical Principles

In the realm of state compression dynamic programming (DP), ordinary binary states (17.1, 17.2) and quaternary plug DP (17.3) are both based on the Fixed-radix Number System. However, when faced with certain non-standard grid constraints, graph coloring problems, or combinatorial optimization problems with dynamic capacity limits, fixed binary or quaternary representations can lead to a large amount of illegal state redundancy (for example: using quaternary to represent at most three states can result in a significant amount of space that can never be effectively mapped, unnecessarily amplifying constants).

To break free from the constraints of fixed bases, we must introduce ternary state compression and a more advanced Minimal Representation based on connected component mapping.

1. Algebraic Isomorphism of Minimal Representation

In Section 17.3, we used bracket matching (left bracket = 1, right bracket = 2) to solve the single loop problem. However, if the problem requires the ability to draw multiple independent loops or to compute merging of connected components in standard graph coloring (such as counting the spanning trees of a tree or connecting multi-colored connected components in a grid), the bracket representation will completely fail. This is because brackets can only express nesting and cannot represent cross-connected topologies such as "plug 1 and plug 3 belong to the same connected component, while plug 2 and plug 4 belong to another connected component".

To eliminate the aftereffect, we must record the specific connected component number to which each plug on the contour line belongs. Suppose there are four plugs on the contour line from left to right, with the actual connectivity being: plugs 0 and 2 are connected, and plugs 1 and 3 are connected. If we directly record this as (1, 2, 1, 2) or (3, 7, 3, 7), it will lead to essentially the same topology being counted as different states due to different numbering.

Core Definition of Minimal Representation:

All positions with plugs on the contour line are renumbered continuously from 1 in the order of first appearance from left to right. Positions without plugs are all recorded as 0.

For the above example, regardless of what the real IDs of the underlying connected components are, after remapping via minimal representation, the state mask uniquely fixes to (1, 2, 1, 2). This geometric dimensionality reduction compresses the explosive set partition number (Bell Number) into an extremely sparse scalar space without loss.

2. Dynamic Number System and State Encoding/Decoding Operators

Since the upper limit of states in minimal representation depends on the number of columns (for example, when $M=6$, at most 3 independent connected components can be produced, with a maximum state value of 3), the most perfect mapping number system at this time is ternary (Base-3) or quinary (Base-5).

To achieve $O(1)$ access in any base during the exam, we need to manually construct a state encoding/decoding operator network beyond bit manipulation:


State Design and Algorithm Derivation

Taking the classic high-difficulty problem "Minimum Spanning Tree (or Multi-loop Grid Skeleton) Based on Connectivity" as an example.

1. State Space and Encoding/Decoding Operator Design

Let the number of columns be $M$. There are $M+1$ positions on the contour line. Define the length of the code array as $M+1$.

2. Reorganization of Connected Components in State Transition (Reconnection Operator)

When progressing through each cell $(i, j)$, extract the left plug $L = code[j-1]$ and the upper plug $U = code[j]$:


C++ Standard Source Code (NOIP/Provincial Selection Ultimate State Compression General Template)

The following source code demonstrates the standard industrial implementation of using minimal representation and dynamic ternary for grid full connected component path coverage (skeleton extraction), supporting large constant squeezing optimizations.

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int HASH_SIZE = 30007; // Choose a large prime to resist collisions
const int MAX_STATES = 200005;

int n, m;
int maze[12][12];
int code[15], temp_code[15];

struct HashTable {
    int head[HASH_SIZE], next[MAX_STATES], size;
    int state[MAX_STATES];
    long long value[MAX_STATES];

    void clear() {
        size = 0;
        memset(head, -1, sizeof(head));
    }

    void insert(int st, long long val) {
        int key = st % HASH_SIZE;
        for (int i = head[key]; i != -1; i = next[i]) {
            if (state[i] == st) {
                value[i] = max(value[i], val); // Here we take the maximum weight/value as an example
                return;
            }
        }
        state[size] = st;
        value[size] = val;
        next[size] = head[key];
        head[key] = size++;
    }
} ht[2];

// Core Operator: Decoding
void decode(int st) {
    for (int i = 0; i <= m; ++i) {
        code[i] = st % 3;
        st /= 3;
    }
}

// Core Operator: Normalize and Encode using Minimal Representation
int encode() {
    int id[15], cnt = 0;
    memset(id, 0, sizeof(id));
    int st = 0;
    for (int i = m; i >= 0; --i) {
        if (code[i] > 0) {
            if (!id[code[i]]) id[code[i]] = ++cnt;
            code[i] = id[code[i]];
        }
        st = st * 3 + code[i];
    }
    return st;
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    if (!(cin >> n >> m)) return 0;

    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= m; ++j) {
            cin >> maze[i][j]; // 1 indicates empty space, 0 indicates obstacle
        }
    }

    int cur = 0;
    ht[cur].clear();
    ht[cur].insert(0, 0); // Initial all 0 state

    for (int i = 1; i <= n; ++i) {
        // When crossing rows, shift the contour line: all plugs move one cell to the right, with 0 added on the left
        for (int k = 0; k < ht[cur].size; ++k) {
            decode(ht[cur].state[k]);
            for (int l = m; l >= 1; --l) code[l] = code[l - 1];
            code[0] = 0;
            ht[cur].state[k] = encode();
        }

        for (int j = 1; j <= m; ++j) {
            int nxt = cur ^ 1;
            ht[nxt].clear();

            for (int k = 0; k < ht[cur].size; ++k) {
                int st = ht[cur].state[k];
                long long val = ht[cur].value[k];

                decode(st);
                int p_left = code[j - 1]; // Left plug
                int p_up = code[j];       // Upper plug

                if (!maze[i][j]) {
                    // Obstacle cell: both plugs must be 0 to pass
                    if (p_left == 0 && p_up == 0) {
                        ht[nxt].insert(st, val);
                    }
                    continue;
                }

                // Core Branch A: No plugs on both sides -> Create a new independent connected component out of thin air
                if (p_left == 0 && p_up == 0) {
                    if (maze[i][j + 1] && maze[i + 1][j]) {
                        code[j - 1] = code[j] = 2; // Temporarily assign an unused independent large number
                        ht[nxt].insert(encode(), val + 1); // Assume weight is the number of edges
                    }
                }
                // Core Branch B: Single plug inflow -> Continue topology
                else if (p_left == 0 || p_up == 0) {
                    int p_val = p_left ? p_left : p_up;
                    if (maze[i][j + 1]) { // Continue to the right
                        code[j - 1] = 0; code[j] = p_val;
                        ht[nxt].insert(encode(), val + 1);
                    }
                    decode(st); // Reset state array for the second branch decision
                    if (maze[i + 1][j]) { // Continue downwards
                        code[j - 1] = p_val; code[j] = 0;
                        ht[nxt].insert(encode(), val + 1);
                    }
                }
                // Core Branch C: Two plugs inflow and belong to different connected components -> Perform strong connected component coloring merge
                else if (p_left > 0 && p_up > 0 && p_left != p_up) {
                    int src = p_up, dst = p_left;
                    for (int l = 0; l <= m; ++l) {
                        if (code[l] == src) code[l] = dst; // Batch coloring
                    }
                    code[j - 1] = code[j] = 0; // Current cell merges and disappears
                    ht[nxt].insert(encode(), val + 1);
                }
                // Core Branch D: Two plugs inflow and are already in the same connected component -> Close loop determination
                else if (p_left > 0 && p_up > 0 && p_left == p_up) {
                    code[j - 1] = code[j] = 0;
                    int remaining_blocks = 0;
                    int check_st = encode();
                    // Check if there are other residual plugs on the contour line
                    if (check_st == 0) {
                        // Completely closed and no other connected components in the entire graph, belongs to a perfect ending solution
                        ht[nxt].insert(0, val + 1);
                    }
                }
            }
            cur = nxt;
        }
    }

    // Collect the maximum benefit from the final completely closed single connected component (state 0)
    long long ans = 0;
    for (int k = 0; k < ht[cur].size; ++k) {
        if (ht[cur].state[k] == 0) {
            ans = max(ans, ht[cur].value[k]);
        }
    }

    cout << ans << "\n";
    return 0;
}

NOIP/Provincial Selection Practical Pitfall Guide

1. Omitting the Clearing and Restructuring of code Array During Encoding (Dirty Data Residue Pollution)

2. Ignoring the Early Abnormal Demise of Isolated Connected Components (Forest Breakage Vulnerability)

Original Source: local://17.4

[h] Back to Home