TuanKiet2004's blog

By TuanKiet2004, history, 2 months ago, In English

Hello everyone!

In this blog, I will describe a solution for E1. String (Easy Version).

The main ideas are:

  • split the infinite sequence into blocks whose length is a power of (k);
  • remove the block-dependent shift using adjacent differences;
  • use the Z-function to compare the pattern with all offsets;
  • use digit DP to count suitable block indices.

    1. Problem Restatement

Define

$$$ s_i=\operatorname{popcount}_k(i)\bmod k, $$$

where (\operatorname{popcount}_k(i)) is the sum of the digits of (i) in base (k).

For each query, we need to count starting positions (i) such that

$$$ l\le i\le r-n+1 $$$

and

$$$ s_i s_{i+1}\ldots s_{i+n-1}=t. $$$

In the code, we define

ll ls = r - n + 1;

If ls < l, then the interval is shorter than the pattern, so the answer is (0).

2. Block Decomposition

Choose (B) as the smallest power of (k) satisfying

$$$ B\ge n. $$$

Thus,

$$$ B=k^m $$$

for some integer (m).

Every index (i) can be written uniquely as

$$$ i=xB+y,\qquad 0\le y \lt B. $$$

Because (B=k^m), multiplying (x) by (B) appends (m) zeroes to the base-(k) representation of (x). Since (y<B), adding (y) does not create a carry into the digits belonging to (x).

Therefore,

$$$ \operatorname{popcount}_k(xB+y) = \operatorname{popcount}_k(x)+\operatorname{popcount}_k(y). $$$

Define

$$$ a_x=\operatorname{popcount}_k(x)\bmod k $$$

and

$$$ b_y=\operatorname{popcount}_k(y)\bmod k. $$$

Then

$$$ s_{xB+y}=(a_x+b_y)\bmod k. $$$

So every block has the same internal structure, and block (x) differs only by the additive shift (a_x).

In the code:

ll B = 1;
while (B < n) B *= k;
int bs = B;

Here:

  • B is the block length;
  • bs is the same value stored as int.

3. Building the Base Block

Let

$$$ b_i=\operatorname{popcount}_k(i)\bmod k. $$$

We use

$$$ \operatorname{popcount}_k(i) = \operatorname{popcount}_k\left(\left\lfloor\frac{i}{k}\right\rfloor\right) +(i\bmod k). $$$

Therefore,

$$$ b_i= \left( b_{\lfloor i/k\rfloor}+i\bmod k \right)\bmod k. $$$

In the code, the base block is stored in blk:

vector<u8> blk(bs, 0);

for (int i = 1; i < bs; ++i) {
    blk[i] = (blk[i / k] + i % k) % k;
}

Since (B) is the smallest power of (k) not smaller than (n),

$$$ B \lt kn. $$$

For the easy version, (k\le 10), so building the block is fast enough.

4. Removing the Block Shift with Differences

Inside block (x),

$$$ s_{xB+y}=(a_x+b_y)\bmod k. $$$

The same value (a_x) is added to all elements of the block.

To eliminate this shift, define adjacent differences.

For the base block:

$$$ bd_i=(blk_{i+1}-blk_i)\bmod k. $$$

For the pattern:

$$$ pd_i=(p_{i+1}-p_i)\bmod k. $$$

In the code:

vector<u8> bd(max(0, bs - 1)), pd(max(0, n - 1));

for (int i = 0; i + 1 < bs; ++i) {
    bd[i] = (blk[i + 1] + k - blk[i]) % k;
}

for (int i = 0; i + 1 < n; ++i) {
    pd[i] = (p[i + 1] + k - p[i]) % k;
}

Suppose the pattern starts at offset off and stays inside one block.

Then the structural condition is

$$$ pd_0pd_1\ldots pd_{n-2} = bd_{off}bd_{off+1}\ldots bd_{off+n-2}. $$$

After the differences match, only the first digit must be checked.

We need

$$$ p_0=(a_x+blk_{off})\bmod k. $$$

Therefore,

$$$ a_x=(p_0-blk_{off})\bmod k. $$$

In the code, this required value is stored in rl:

int rl = (p[0] + k - blk[off]) % k;

5. Comparing All Offsets with the Z-function

Checking every offset independently would be too slow.

The function

getLCP(p, t, sep)

runs the Z-function on

$$$ p+\text{separator}+t $$$

and returns

res[i] = LCP(p, t[i...]).

We compute:

vector<int> L, R;

if (!bd.empty()) L = getLCP(pd, bd, k);
if (!pd.empty()) R = getLCP(bd, pd, k);

Therefore:

  • L[off] is the LCP between pd and bd[off...];
  • R[pos] is the LCP between bd and pd[pos...].

The separator is k, which is outside the valid digit range ([0,k-1]).

6. A Match Crosses at Most One Block Boundary

Because

$$$ n\le B, $$$

a substring of length (n) can only:

  • lie completely inside one block;
  • cross exactly one boundary between two consecutive blocks.

It cannot cross two boundaries.

Case 1: The Pattern Lies Completely Inside One Block

This happens when

off + n <= bs

The difference sequence must match:

bool m = (n == 1) || (L[off] >= n - 1);

If it matches, then the block index (x) must satisfy

$$$ a_x=rl. $$$

We count valid offsets in

sb[lt][ht][rl]

using

if (m) ++sb[lt][ht][rl];

Here, sb means “same block”.

Case 2: The Pattern Crosses One Boundary

Let

int fl = bs - off;

Then fl is the number of pattern digits placed in the first block.

The pattern is split into:

  • p[0 ... fl - 1], placed at the end of block (x);
  • p[fl ... n - 1], placed at the beginning of block (x+1).

The number of differences in the left part is

int ldc = fl - 1;

and the number of differences in the right part is

int rdc = n - fl - 1;

The left part is valid when

bool lv = (ldc <= 0) || (L[off] >= ldc);

The right part is valid when

bool rv = (rdc <= 0) || (R[fl] >= rdc);

For the first block, we still need

$$$ a_x=rl. $$$

For the second block, the pattern starts at offset (0), and

$$$ blk_0=0. $$$

Therefore,

$$$ a_{x+1}=p_{fl}. $$$

In the code, this value is stored in rr:

int rr = p[fl];

We count valid crossing offsets in

cb[lt][ht][rl][rr]

using

++cb[lt][ht][rl][rr];

Here, cb means “cross block”.

7. Restricting Starting Positions

Every starting position has the form

$$$ i=xB+off. $$$

The valid starting positions are in

$$$ [l,ls]. $$$

In the code:

ll lb = l / B, rb = ls / B;
int lo = l % B, ro = ls % B;

Here:

  • lb is the block containing (l);
  • rb is the block containing ls;
  • lo is the offset of (l) in block lb;
  • ro is the offset of ls in block rb.

For a fixed offset off:

  • if off < lo, the first valid block is lb + 1;
  • otherwise, it is lb;

and:

  • if off > ro, the last valid block is rb - 1;
  • otherwise, it is rb.

Therefore, define

int lt = (off < lo);
int ht = (off > ro);

Then the valid block index interval is

$$$ [lb+lt,\ rb-ht]. $$$

There are only four possible pairs ((lt,ht)), so all offsets can be grouped into four categories.

8. What the Digit DP Must Count

For a block index (x), define

$$$ a_x=\operatorname{popcount}_k(x)\bmod k. $$$

We need two kinds of counts.

First:

$$$ \#\{x\mid a_x=A\}. $$$

Second:

$$$ \#\{x\mid a_x=A,\ a_{x+1}=C\}. $$$

The structure

struct C {
    ll c1[10]{};
    ll c2[10][10]{};
};

stores:

  • c1[a]: the number of (x) with (a_x=a);
  • c2[a][b]: the number of (x) with (a_x=a) and (a_{x+1}=b).

The function

C cp(ll lim)

computes these values for all

$$$ 0\le x\le lim. $$$

9. Relation Between \(x\) and \(x+1\)

Suppose the base-(k) representation of (x) ends with (c) digits equal to (k-1).

When adding (1):

  • those (c) digits become (0);
  • the previous digit increases by (1).

Therefore,

$$$ \operatorname{popcount}_k(x+1) = \operatorname{popcount}_k(x)-c(k-1)+1. $$$

Modulo (k),

$$$ -c(k-1)\equiv c\pmod k. $$$

Hence,

$$$ \operatorname{popcount}_k(x+1) \equiv \operatorname{popcount}_k(x)+c+1 \pmod k. $$$

So the digit DP only needs to remember:

  • the digit sum modulo (k);
  • the number of trailing digits equal to (k-1), modulo (k).

10. Digit DP State

The state is

dp[t][s][r]

where:

  • t is the tight flag;
  • s is the current digit sum modulo (k);
  • r is the current number of trailing digits equal to (k-1), modulo (k).

The initialization is

dp[1][0][0] = 1;

For every next digit x:

int nt = t && (x == ld);
int ns = (s + x) % k;
int nr = (x == k - 1) ? (r + 1) % k : 0;

After all digits are processed:

ans.c1[s] += w;

because (a_x=s).

Also,

int ns = (s + 1 + r) % k;
ans.c2[s][ns] += w;

because

$$$ a_{x+1}=(a_x+1+r)\bmod k. $$$

11. Combining the Results

For each pair ((lt,ht)), the valid block interval is

ll low = lb + lt;
ll high = rb - ht;

If low > high, this group contributes nothing.

Otherwise, compute prefix statistics:

C ph = cp(high);
C pl = cp(low - 1);

For matches inside one block, the number of valid block indices with (a_x=a) is

ll cs = ph.c1[a] - pl.c1[a];

so the contribution is

ans += sb[lt][ht][a] * cs;

For matches crossing a boundary, the number of valid block indices with

$$$ a_x=a,\qquad a_{x+1}=b $$$

is

ll cp2 = ph.c2[a][b] - pl.c2[a][b];

so the contribution is

ans += cb[lt][ht][a][b] * cp2;

12. Correctness Proof

Lemma 1

For (B=k^m), (i=xB+y), and (0\le y<B),

$$$ s_i=(a_x+b_y)\bmod k. $$$

Proof.

Multiplying (x) by (k^m) appends (m) zeroes to its base-(k) representation. Since (y<k^m), adding (y) does not affect the digits of (x). Therefore, the digit sum of (xB+y) is the sum of the digit sums of (x) and (y). (\square)

Lemma 2

A substring of length (n\le B) crosses at most one block boundary.

Proof.

Two consecutive block boundaries are (B) positions apart. A substring of length at most (B) cannot cross two boundaries. (\square)

Lemma 3

A pattern segment lying inside one block matches if and only if its first digit and all adjacent differences match.

Proof.

The first digit together with all consecutive differences uniquely determines every following digit modulo (k). (\square)

Lemma 4

For a pattern crossing one boundary, matching the left differences, the right differences, rl, and rr is necessary and sufficient.

Proof.

The left and right parts each lie completely inside one block. By Lemma 3, each part is uniquely determined by its first digit and its internal differences. The values rl and rr provide the required first digits for the two blocks. (\square)

Lemma 5

The digit DP in cp(lim) correctly computes c1 and c2.

Proof.

The DP enumerates all base-(k) numbers from (0) to lim. State s stores the digit sum modulo (k), and state r stores the number of trailing digits equal to (k-1), modulo (k). Therefore, the formula

$$$ a_{x+1}=(a_x+1+r)\bmod k $$$

gives the correct value for the next block. Hence c1 and c2 are computed correctly. (\square)

Theorem

The algorithm returns the exact number of occurrences of (t) in (s_l s_{l+1}\ldots s_r).

Proof.

By Lemma 2, every occurrence either stays inside one block or crosses exactly one boundary. The arrays L and R verify all required difference sequences. The arrays sb and cb group all valid offsets, while cp counts all block indices satisfying the required block values. Thus, every valid occurrence is counted exactly once, and no invalid occurrence is counted. (\square)

13. Complexity

Let (B) be the smallest power of (k) satisfying (B\ge n).

Building blk, bd, and pd takes

$$$ O(B+n). $$$

The two Z-function executions take

$$$ O(B+n). $$$

Processing all offsets takes

$$$ O(B). $$$

The digit DP has:

  • (O(\log_k r)) digit positions;
  • (2k^2) states;
  • at most (k) transitions from each state.

Therefore, one call to cp takes

$$$ O(k^3\log_k r). $$$

Only a constant number of cp calls are needed.

The total time complexity per query is

$$$ \boxed{O(B+n+k^3\log_k r)}. $$$

Since (B<kn), this can also be written as

$$$ \boxed{O(kn+k^3\log_k r)}. $$$

The memory complexity is

$$$ \boxed{O(B+n+k^2)}. $$$

14. Implementation

https://codeforces.me/contest/2249/submission/384475090

Thanks for reading!

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

»
2 months ago, hide # |
 
Vote: I like it 0 Vote: I do not like it

I'm new here and I'd really appreciate any feedback from everyone.