国庆模拟赛第六场题解

~ 2026-10-7 16:21:40

长方形

50分

枚举四条边的长度(或只枚举较短边 aa),统计满足条件的方案数,时间复杂度 O(n)O(n),可以通过子任务 1,21,2。

核心代码:

if (n & 1) { cout << 0 << '\n'; return 0; }
long long s = n / 2, ans = 0;
for (long long a = 1; a < s; ++a) if (a < s - a) ++ans;
cout << ans << '\n';

100分

设四段长度从小到大为 a≤b≤c≤da \le b \le c \le d。要用它们组成矩形,四条边必须两两相等,即 a=ba = b 且 c=dc = d;又不能是正方形,所以 b≠cb \neq c。于是

a+b+c+d=2(a+c)=na + b + c + d = 2(a + c) = n

记短边为 xx、长边为 yy,则 x+y=n2x + y = \dfrac{n}{2} 且 1≤x<y1 \le x < y。

  • 若 nn 为奇数,2(x+y)=n2(x+y) = n 无整数解,答案为 00;
  • 否则令 s=n2s = \dfrac{n}{2},xx 可取 $1, 2, \dots, \left\lfloor \dfrac{s-1}{2} \right\rfloor$,答案就是 ⌊s−12⌋\left\lfloor \dfrac{s-1}{2} \right\rfloor。

时间复杂度 O(1)O(1)。

核心代码:

long long n;
cin >> n;
if (n & 1) { cout << 0 << '\n'; return 0; }
long long s = n / 2;
cout << (s - 1) / 2 << '\n';

常见错误

  • 忽略 nn 为奇数时无解,直接输出 n/4n / 4。例如 n=11n = 11 会算成 22,正确答案是 00。
  • 忘记排除正方形:n=4tn = 4t 时不能把 (t,t,t,t)(t,t,t,t) 计入,答案应为 t−1t - 1 而不是 tt。

充值会员

40分

n≤1000n \le 1000 时,第 ii 天直接回扫所有 j≤ij \le i,看是否存在 j+aj−1≥ij+a_j-1 \ge i 即可。

时间复杂度 O(n2)O(n^2)。

核心代码:

for (int i = 1; i <= n; i++) {
    bool own = false, bor = false;
    for (int j = 1; j <= i; j++)
        if (a[j] && j + a[j] - 1 >= i) own = true;
    for (int j = 1; j <= i; j++)
        if (b[j] && j + b[j] - 1 >= i) bor = true;
    if (own) x++; else if (bor) y++; else z++;
}

100分

关键观察:第 ii 天小明自己有会员,当且仅当存在一个 j≤ij \le i 使得 j+aj−1≥ij+a_j-1 \ge i。因为所有可选的 jj 都满足 j≤ij \le i,这个条件等价于

max⁡j≤i(j+aj−1)≥i.\max_{j \le i}\bigl(j+a_j-1\bigr) \ge i .

所以只需维护两个量:r0r_0 为已处理过的充值里小明最晚的到期日,r1r_1 为小红最晚的到期日。扫到第 ii 天时先把 i+ai−1i+a_i-1、i+bi−1i+b_i-1 更新进去(注意 ai=0a_i=0 表示当天没充值,不能更新),再比较即可。

  • r0≥ir_0 \ge i:自己有会员,xx 加一;
  • 否则 r1≥ir_1 \ge i:借小红的,yy 加一;
  • 否则 zz 加一。

时间复杂度 O(n)O(n),空间 O(n)O(n)。

核心代码:

int r0 = 0, r1 = 0, x = 0, y = 0, z = 0;
for (int i = 1; i <= n; i++) {
    if (a[i]) r0 = max(r0, i + a[i] - 1);
    if (b[i]) r1 = max(r1, i + b[i] - 1);
    if (r0 >= i) x++;
    else if (r1 >= i) y++;
    else z++;
}

常见错误

  • 把 ai=0a_i=0 也拿去更新最晚到期日,凭空多出一天会员期。
  • 认为充值会"从下一天开始生效",本题明确从当天算起,覆盖 [i, i+ai−1][i,\,i+a_i-1]。
  • 误以为要模拟"到期后重新充值"的叠加,实际上多次充值取最晚到期日就够了。

菜品研发

30分

n=1n = 1 时答案就是 ⌊(a1+kc)/b1⌋\left\lfloor (a_1 + kc)/b_1 \right\rfloor;答案不超过 10410^4 时,从 00 开始一个个枚举答案,每次 O(n)O(n) 判定即可。

判定:做 xx 道菜需要第 ii 种食材 xbix b_i 个,缺口为 di=max⁡(0, xbi−ai)d_i=\max(0,\,xb_i-a_i),每包魔法粉变出 cc 个同一种食材,所以第 ii 种需要 ⌈di/c⌉\lceil d_i/c\rceil 包,总和不超过 kk 就可行。

时间复杂度 O(n⋅答案)O(n\cdot \text{答案})。

核心代码:

bool ok(ll x) {
    ll used = 0;
    for (int i = 1; i <= n; i++) {
        ll d = x * b[i] - a[i];
        if (d > 0) { used += (d + c - 1) / c; if (used > k) return false; }
    }
    return used <= k;
}

100分

能做的菜越多,需要的魔法粉越多,可行性关于 xx 单调,于是二分答案。每次判定 O(n)O(n),总复杂度 O(nlog⁡Ans)O(n\log \text{Ans}),Ans\text{Ans} 上界取 4×10184\times 10^{18} 足够(答案不超过 min⁡i(ai+kc)/bi≤1018+109\min_i (a_i+kc)/b_i \le 10^{18}+10^9)。

注意 xx 最大可达 101810^{18} 量级,x⋅bix\cdot b_i 会爆 long long。两种稳妥写法:

  • 全程用 __int128 算 xbi−aixb_i-a_i;
  • 或像 std 那样先拆成 ai/bia_i/b_i 与 ai mod bia_i\bmod b_i,只在"剩余魔法粉足够"时才去算 need⋅bineed\cdot b_i。

时间复杂度 O(nlog⁡Ans)O(n\log \text{Ans})。

核心代码(__int128\_\_int128):

typedef __int128 lll;
bool ok(ll x) {
    lll used = 0;
    for (int i = 1; i <= n; i++) {
        lll d = (lll)x * b[i] - a[i];
        if (d > 0) { used += (d + (lll)c - 1) / (lll)c; if (used > (lll)k) return false; }
    }
    return used <= (lll)k;
}
ll l = 0, r = 4000000000000000000LL;
while (l < r) { ll mid = l + (r - l + 1) / 2; if (ok(mid)) l = mid; else r = mid - 1; }

核心代码(先判断再乘防止爆ll):

const int maxn = 2e5 + 5;
ll n,m,k,c,ans;
ll a[maxn],b[maxn];
int jud(ll x){
    ll res = k;
    for(int i = 1; i <= n; i++){
        ll need = x - (a[i] / b[i]);
        if(need <= 0) continue;
        if((res * c + a[i] % b[i]) / b[i] < need) return 0;
        res -= (need * b[i] - a[i] % b[i] + c - 1) / c;
    }
    return 1;
}
int main(){
    cin>>n>>k>>c;
    for(int i = 1; i <= n; i++) cin>>a[i];
    for(int i = 1; i <= n; i++) cin>>b[i];
    ll l = 0,r = inf;
    while(l < r){
        ll mid = (l + r) >> 1;
        if(jud(mid)) l = mid + 1,ans = mid;
        else r = mid;
    }
    cout<<ans<<endl;
    return 0;
}

常见错误

  • 用 int 存答案。样例 4 的答案是 1018+110^{18}+1,必须用 long long。
  • 把 kk 包魔法粉当成 kckc 个任意分配的食材(那样只需比较总量)。实际上每包只能变同一种,因此必须对每种食材分别上取整后再求和。
  • 判定里写 di/cd_i/c 而不是 ⌈di/c⌉\lceil d_i/c\rceil,会高估可做的菜数。

33DAI 的代码

暴力枚举

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200000;
// 表示食材数、魔法粉数、消耗一包魔法粉可以获得的食材数量
int n, k, c;
int a[MAXN + 5]; // 第 i 种食材的数量
int b[MAXN + 5]; // 研发一道菜品需要消耗第 i 种食材的数量。
// 能不能做出来 x 道菜
bool check(long long x)
{
    __int128 cnt = 0; // 记录要用多少魔法粉
    for (int i = 1; i <= n; i++)
    {
        __int128 need = (__int128)x * b[i] - a[i]; // 还要这么多个食材 i
        if (need <= 0)
            continue;
        // 需要 need/c 上取整这么多魔法粉
        cnt += (need + (c - 1)) / c;
    }
    return cnt <= k;
}
int main()
{
    freopen("dish.in", "r", stdin);
    freopen("dish.out", "w", stdout);
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> k >> c;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    for (int i = 1; i <= n; i++)
        cin >> b[i];
    for (long long ans = 1;; ans++)
        if (!check(ans)) // ans道菜做不出来
        {
            cout << ans - 1;
            return 0;
        }
    return 0;
}

100 分

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200000;
// 表示食材数、魔法粉数、消耗一包魔法粉可以获得的食材数量
int n, k, c;
int a[MAXN + 5]; // 第 i 种食材的数量
int b[MAXN + 5]; // 研发一道菜品需要消耗第 i 种食材的数量。
// 能不能做出来 x 道菜
bool check(long long x)
{
    __int128 cnt = 0; // 记录要用多少魔法粉
    for (int i = 1; i <= n; i++)
    {
        __int128 need = (__int128)x * b[i] - a[i]; // 还要这么多个食材 i
        if (need <= 0)
            continue;
        // 需要 need/c 上取整这么多魔法粉
        cnt += (need + (c - 1)) / c;
    }
    return cnt <= k;
}
int main()
{
    freopen("dish.in", "r", stdin);
    freopen("dish.out", "w", stdout);
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> k >> c;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    for (int i = 1; i <= n; i++)
        cin >> b[i];
    long long L = 0;
    long long R = 1'000'000'001'000'000'000;
    long long ans = 0;
    while (L <= R)
    {
        long long mid = (L + R) / 2;
        if (check(mid))
            ans = mid, L = mid + 1;
        else
            R = mid - 1;
    }
    cout << ans;
    return 0;
}

环形赛场

100分

来源:原创

破环成链:编号最小的开放城市固定为 xx,故把 aa 倍长为 a1…a2na_1\ldots a_{2n},所有讨论都落在链 [x, x+n][x,\,x+n] 上。

设 dpi,jdp_{i,j} 表示开放了 jj 个城市、其中编号最大的开放城市是 ii 时,目标城市在 [x,i][x,i] 中的选手消耗的体力之和的最小值。

  • 初始化:dpx,1=0dp_{x,1} = 0;
  • 转移:$dp_{i,j} = \min\limits_{x \le p < i}\{dp_{p,j-1} + cost_{p,i}\}$;
  • 答案:min⁡{dpi,j+costi, n+x}\min\{dp_{i,j} + cost_{i,\,n+x}\},末段 [i,n]∪[1,x][i,n] \cup [1,x] 的选手只能从 ii 或 xx 进入。

注意受 xx 的限制,未必能开满 kk 个城市,所以要对 j≤kj \le k 一并取 min⁡\min。

如何求 costcost:costp,icost_{p,i} 是从 pp 或 ii 进入、目标在 [p,i][p,i] 的选手的消耗。[ p,⌊p+i2⌋ ][\,p,\lfloor\frac{p+i}{2}\rfloor\,] 离 pp 更近,其余离 ii 更近,所以 costp,icost_{p,i} 等于一段"前缀平方和"拼一段"后缀平方和",正反各递推一遍即可 O(n2)O(n^2) 全部预处理。

复杂度 O(n2+n2k)O(n^2 + n^2k),空间 O(n2)O(n^2)(约 128 MiB128\,\mathrm{MiB})。

子任务递进

子任务 做法 复杂度
k=1k=1 无需 DP,答案即 costx, n+xcost_{x,\,n+x} O(n)O(n)
k≤2k \le 2 枚举第二个开放城市 O(n2)O(n^2)
n≤100n \le 100 costcost 用 O(n3)O(n^3) 朴素三重循环预处理 O(n3)O(n^3)
正解 costcost 前缀和 + 后缀和优化 O(n2k)O(n^2k)

常见错误

  1. 当成"恰好 kk 个":受 xx 限制可能开不满,必须对所有 j≤kj \le k 取 min⁡\min。
  2. 末段算漏:[i,n]∪[1,x][i,n]\cup[1,x] 的选手在 ii 与 xx 之间择优,不能只算从 ii 进入。
  3. 用 int 存答案:n=2×103, ai=106n=2\times10^3,\ a_i=10^6 时答案可达 3×10143\times10^{14},必须用 long long。

核心代码:

void solve(){
    read(n);read(k);read(x);
	rep(i,1,n){
        read(a[i]);
    }
	rep(i,n + 1,2 * n) a[i] = a[i - n];
	rep(i,1,2 * n){
        c[i] = 0;
		rep(j,i + 1,2 * n){
            c[j] = c[j - 1] + a[j] * (j - i) * (j - i);
			d[i][j] = c[(i + j) / 2];
		}
	}
    dep(i,n * 2,1){
        c[i] = 0;
       	dep(j,i - 1,1){
            c[j] = c[j + 1] + a[j] * (i - j) * (i - j);
			d[j][i] += c[(i + j + 2) / 2];
		}
    }
	rep(i,0,n) rep(j,0,k) dp[i][j] = inf;
    dp[x][1] = 0;
	ll ans = d[x][x + n];
	rep(i,x + 1,n){
		rep(j,2,k){
			dep(pre,i - 1,x){
				add(dp[i][j],dp[pre][j - 1] + d[pre][i]);
			}
		}
        rep(j,2,k) add(ans,dp[i][j] + d[i][n + x]);
	}
	printf("%lld\n",ans);
}

略策游戏

100分

来源:原创

关键观察

由于 Ai,Bj≥0A_i,B_j\ge 0,对固定的行 ii,小明要让 Ci,j=Ai×BjC_{i,j}=A_i\times B_j 最小,就一定会选该行未被选过的 BjB_j 最小的那一列(题面也明确了这条规则)。

于是每行被反复选中时,被划掉的一定是排好序后最靠前的那几个 BB。设第 ii 行已被选走 cnticnt_i 次,则它下一次的得分为

$$\text{val}_i=\begin{cases}A_i\times B_{cnt_i+1}, & cnt_i<m\\[4pt] 0,&cnt_i=m\end{cases}$$

其中 BB 已按升序排好。

做法

把 BB 升序排序,用一棵线段树维护所有行的 vali\text{val}_i,每个节点额外记录取得该最大值时最小的行号(合并时平局取左子树,即可保证这一点)。

  • 游戏轮 [x,y][x,y]:在线段树上查询区间最大值及其对应的最小行号 idid,输出(注意答案要和 00 取 max⁡\max),然后 cntid←cntid+1cnt_{id}\gets cnt_{id}+1 并单点更新该行。
  • 修改轮 Ax←yA_x\gets y:只影响尚未被选过的位置,单点重新计算第 xx 行即可;若 cntx=mcnt_x=m,这一行无论怎么改都是 00,可跳过。

总复杂度 O((n+q)log⁡n+mlog⁡m)O\big((n+q)\log n+m\log m\big)。

核心代码

// 合并时平局取左子树,保证 pos 是取得最大值的最小行号
void pu(int rt){
    if(tr[ls].mx >= tr[rs].mx) tr[rt].mx = tr[ls].mx, tr[rt].pos = tr[ls].pos;
    else                       tr[rt].mx = tr[rs].mx, tr[rt].pos = tr[rs].pos;
}

sort(b + 1, b + 1 + m);
rep(i, 1, n) c[i] = 1;                      // c[i] = 下一次会取到第几小的 B
build(1, 1, n);
while(q--){
    read(op); read(x); read(y);
    if(op == 1){
        int id = tr[qry(1, 1, n, x, y)].pos;
        printf("%lld\n", max(0LL, query_max(x, y)));
        c[id]++;
        upd(1, 1, n, id, c[id] <= m ? a[id] * b[c[id]] : 0);
    }else{
        a[x] = y;
        if(c[x] <= m) upd(1, 1, n, x, a[x] * b[c[x]]);
    }
}

子任务提示

  • 没有修改轮时,每行的 cntcnt 只会增长、AA 不变,静态维护即可(子任务 2,42,4)。
  • 有修改轮时才需要在修改后重新计算单行的 val\text{val}(子任务 3,53,5)。
  • n,m,q≤103n,m,q\le 10^3 时也可以每次线性扫一遍区间,不需要线段树。

边界讨论

  • 某行的格子全部被选走后得分为 00,它与其他得 00 的行一起参与「行号最小」的比较。
  • BB 中出现 00 时,所有行首次得分都是 00,此时靠「行号最小」选出第 xx 行。
  • Ai=0A_i=0 的行得分恒为 00。
  • n=1n=1 或 m=1m=1 时区间退化,注意 qry 返回值的合并写法。

亡羊补栏 —— 题解

一、题意重述与建模

nn 块木板排成一排,第 ii 块状态 ai∈{0,1}a_i \in \{0, 1\}。买一块长度为 LL 的补丁可以盖住一段连续的 LL 块,价格为 ⌊L⌋\lfloor \sqrt{L} \rfloor;补丁可以压在完好木板上,但必须完整落在 [1,n][1, n] 内。要求所有 ai=0a_i = 0 的位置都被盖住,求最小总价。

把每一段极长的连续 0 称为一个零段,设共有 mm 段,第 ii 段为 [ℓi,ri][\ell_i, r_i](11-indexed,段之间不相邻,ℓ\ell 与 rr 都严格递增)。

二、三条关键引理

引理 1(同价最长)

花费 cc 元能买到的补丁长度 LL 满足 ⌊L⌋=c\lfloor \sqrt{L} \rfloor = c,即 c2≤L≤(c+1)2−1c^2 \le L \le (c+1)^2 - 1。

取 L=(c+1)2−1L = (c+1)^2 - 1 总是不劣(盖得更长绝不会更差)。所以"花 cc 元"等价于"获得 (c+1)2−1(c+1)^2 - 1 的覆盖长度"。

常见错误:只考虑 L=c2L = c^2。这样会低估每一档的能力,例如 L=8L = 8 时 ⌊8⌋=2\lfloor\sqrt{8}\rfloor = 2,但只考虑平方数的人会以为 22 元只能买 L=4L = 4,答案偏大。

引理 2(补丁互不重叠,且按左端点有序)

设两块补丁 A=[a1,a2]A = [a_1, a_2]、B=[b1,b2]B = [b_1, b_2] 满足 a1≤b1≤a2a_1 \le b_1 \le a_2(重叠)。把 BB 改成 [a2+1, b2][a_2 + 1,\ b_2]:

  • 区间 (a2,b2](a_2, b_2] 中的破损点原本由 BB 负责,改后仍由 BB 负责;
  • 区间 [b1,a2][b_1, a_2] 中的破损点已被 AA 盖住,不需要 BB 再盖;
  • 新 BB 的长度 ≤\le 原长度,花费不增。

反复做这个操作,可化为若干块互不重叠、按左端点从左到右排列的补丁。

引理 3(每块补丁都从某个零段的左端点开始)

在引理 2 的形态下,考虑最靠左的、还没被前面补丁盖住的零段 [ℓj,rj][\ell_j, r_j]。负责盖住 ℓj\ell_j 的那块补丁,其左端点 l≤ℓjl \le \ell_j。把它整体右移到 l′=ℓjl' = \ell_j:

  • [l,ℓj)[l, \ell_j) 中没有需要它盖的破损点;
  • 右端点同步右移,覆盖只会更多,花费不变。

所以每块补丁可以假定从某个 ℓj\ell_j 开始。

由引理 2、3,最优解必然是:

把 mm 个零段按先后顺序切成若干连续的组,每一组用一块补丁,从该组第一段的 ℓ\ell 一直盖到该组最后一段的 rr。

于是问题变成:

$$\mathrm{dp}[i] = \min_{1 \le j \le i}\Big(\mathrm{dp}[j-1] + \big\lfloor\sqrt{r_i - \ell_j + 1}\big\rfloor\Big), \qquad \mathrm{dp}[0] = 0$$

答案即 dp[m]\mathrm{dp}[m](m=0m = 0 时答案为 00)。

三、O(m2)O(m^2) 朴素段 DP —— 过 S1~S3

直接按上式做,见 savedata.cpp。

  • S3(n≤103n \le 10^3,m≤500m \le 500):2.5×1052.5\times 10^5 次转移,轻松通过;
  • S4(n≤3×104n \le 3\times 10^4,m≤1.5×104m \le 1.5\times 10^4):2.25×1082.25\times 10^8 次转移,超时。

四、O(mnlog⁡n)O(m\sqrt{n}\log n) 桶优化 —— 过 S1~S4

固定 jj,随着 ii 增大 rir_i 单调增大,所以

$$c(j, i) = \big\lfloor\sqrt{r_i - \ell_j + 1}\big\rfloor$$

关于 ii 单调不减。维护每个 jj 当前所在的"桶" cur_c[j]\mathrm{cur\_c}[j],以及 $\mathrm{bucket}[c] = \{\mathrm{dp}[j-1] \mid \mathrm{cur\_c}[j] = c\}$(有序多重集),则

$$\mathrm{dp}[i] = \min_{c}\ \big(c + \min \mathrm{bucket}[c]\big)$$

跨桶事件:段 jj 会离开桶 cc 当且仅当 ri≥ℓj+(c+1)2−1r_i \ge \ell_j + (c+1)^2 - 1。用优先队列按"触发的 ii"排序,配合在 rr 数组上二分找出触发位置。每个 jj 至多跨 n\sqrt{n} 次桶,总复杂度 O(mnlog⁡n)O(m\sqrt{n}\log n)。

两个必踩的坑:

  1. 入桶时机:dp[j−1]\mathrm{dp}[j-1] 只有在循环走到 i=ji = j 时才已经算好。必须在第 ii 步先把段 ii 入桶,再算 dp[i]\mathrm{dp}[i]。第一版在初始化时就把所有 dp[j−1]\mathrm{dp}[j-1] 入桶,结果全是 INT_MIN。

  2. 一次事件可能连跳多级:不能写 new_c = old_c + 1。因为 rir_i 是跳跃增长的,触发时真实代价应该重新计算:

    int new_c = isq((long long)r[i] - l[j] + 1);   // 正确
    // int new_c = old_c + 1;                      // 错误:会严重低估代价
    

    实测反例:55.in 的第 11 段 [ℓ1,r1]=[1,10][\ell_1, r_1] = [1, 10] 起步 c=3c = 3,当下一段的 rr 跳到 142142 时真实代价已是 ⌊142⌋=11\lfloor\sqrt{142}\rfloor = 11,而 +1 只得到 44,答案从 2525 掉到 88。

五、O(n)O(n) 正解 —— 过全部子任务

换个角度,按总花费做 DP。

定义:f[v]f[v] = 总花费不超过 vv 时,能从位置 11 起"连续覆盖"到的最右端点,即 [1,f[v]][1, f[v]] 内所有 0 都已被盖住的最大 f[v]f[v]。

转移:最后一块补丁花 cc 元(1≤c≤v1 \le c \le v)。由引理 1,它能盖 (c+1)2−1(c+1)^2 - 1 长;由引理 3,它应从上一个还没盖住的 0 开始。记

$$\mathrm{nxt}[x] = \text{位置 } x \text{ 及其右侧第一个 } 0 \text{ 的位置(没有则为 } n+1)$$

则这一块的起点 st=nxt[f[v−c]+1]\mathrm{st} = \mathrm{nxt}[f[v-c] + 1],覆盖 [st, st+L−1][\mathrm{st},\ \mathrm{st} + L - 1],之后第一个可能没盖到的位置是 st+L\mathrm{st} + L,于是

$$f[v] = \max\Big(f[v-1],\ \max_{1 \le c \le v}\ \big(\mathrm{nxt}[\mathrm{st} + L] - 1\big)\Big),\qquad L = (c+1)^2 - 1$$

(st+L>n\mathrm{st} + L > n 时取 nn。)

答案:设 last0\mathrm{last}_0 是最后一个 0 的位置,答案是最小的 vv 使 f[v]≥last0f[v] \ge \mathrm{last}_0。没有 0 时答案为 00。

为什么 vv 只需枚举到 ⌊n⌋\lfloor\sqrt{n}\rfloor:一块长为 last0≤n\mathrm{last}_0 \le n 的补丁就能盖住全部破损,代价 $\lfloor\sqrt{\mathrm{last}_0}\rfloor \le \lfloor\sqrt{n}\rfloor$。

复杂度:vv 上界 V=⌊n⌋V = \lfloor\sqrt{n}\rfloor,总转移次数 ∑v=1Vv=O(V2)=O(n)\sum_{v=1}^{V} v = O(V^2) = O(n)。预处理 nxt\mathrm{nxt} 也是 O(n)O(n)。n=107n = 10^7 时 V=3162V = 3162,转移约 5×1065\times 10^6 次,实测 0.15 s。

内存:n=107n = 10^7 时,char s[n] 约 10 MiB + int nxt[n+2] 约 40 MiB ≈ 50 MiB,256 MiB 足够。不要把 nxt 也开成 char 以外的多个 int 大数组,容易爆内存。

两个 off-by-one(重写时实测踩到)

  1. 起点必须是"下一个没盖住的 0",不是 f[v−c]+1f[v-c] + 1。若 f[v−c]+1f[v-c]+1 处是 1,从那里起贴会白白浪费长度。反例:n=9n = 9、s = 100000000,零段在 [2,9][2, 9],正解是 22(一块长 88 的补丁贴 [2,9][2,9]),若从 11 起贴会得到 33。
  2. 覆盖区间是 [st,st+L−1][\mathrm{st}, \mathrm{st}+L-1],所以"第一个可能没盖到的位置"是 st+L\mathrm{st} + L,不是 st+L+1\mathrm{st} + L + 1。反例:n=9n = 9、s = 000000000,正解是 33(长 99 的补丁),若多算一位会得到 22。

六、边界与易错点清单

# 易错点 表现
1 用 L=c2L = c^2 而不是 (c+1)2−1(c+1)^2 - 1 答案偏大
2 用 ⌈L⌉\lceil\sqrt{L}\rceil 计算代价 答案偏大(应为 ⌊L⌋\lfloor\sqrt{L}\rfloor)
3 默认"每个零段单独打一块" 交替数据上答案爆炸
4 默认"一块盖满全场" 簇状数据上答案偏大
5 桶优化里 new_c = old_c + 1 答案偏小(最难查的一类)
6 桶优化里提前把未算好的 dp[j−1]\mathrm{dp}[j-1] 入桶 答案为 INT_MIN
7 补丁起点取 f+1f+1 而非下一个 0 答案偏大
8 覆盖右端点多算/少算 1 答案偏大或偏小
9 全 1 时未特判 未输出 00
10 数组大小按 10510^5 开 S5 段错误
11 Windows 下用 fopen(..., "w") 生成数据 数据带 CRLF,Linux 评测机读入异常

七、参考代码

const int maxn = 1e7 + 5;
int n,m,k,q;
int a[maxn];
int ans,tmp,cnt;
int dp[maxn],p[maxn];
char s[maxn];
void solve(){
    read(n);
    scanf("%s",s + 1);
    rep(i,1,n) a[i] = (s[i] & 1);
    a[n + 1] = 0;
    p[n + 1] = n + 1;
    dep(i,n,0){
        p[i] = p[i + 1];
        if(!a[i+1]) p[i] = i+1;
    }
    int i = 0,ans = n;
    dp[0] = p[0] - 1;
    while(dp[i] < n){
        i++;
        dep(j,i - 1,0) {
            dp[i] = max(dp[i],p[min(n + 1,dp[j] + (i - j + 1) * (i - j + 1) - 1)] - 1);
        }
    }
    printf("%d\n",i);
}
int main(){
    //ios::sync_with_stdio(0);cin.tie(0);
    int T=1;
    //read(T);
    while(T--) solve();
    return 0;
}


我们会审查剪贴板内容,并对发布不合适内容的同学进行相应的处理