2026.9 做题记录4

9.21

[JOIST 2025] 多方通信 / Multi Communication

题意

这是一道提交答案题。

有 \(N\) 个人在进行游戏,初始时,裁判秘密指定其中一个人为“目标”(除了目标本身外,其他人不知道目标是谁)。

每人有一块白板。游戏共进行 \(L\) 个回合,每回合:

  1. 每人根据自己已知的信息,在白板上写下符号 T 或 F。
  2. 每人选择一个参与者,查看其白板上的符号。

\(L\) 个回合结束后,所有人都必须正确猜出目标是谁。

要求设计一个策略,使得无论目标是谁,所有人最终都能正确猜出,且回合数 \(L\) 尽可能小。

你需要输出:对于每个人作为初始选中的目标,给出每个参与者在每回合写下的符号和选择查看的对象。

\(N=4,32,48\),满分要求分别为 \(L\le 2,8,9\)。

solution

好玩,感觉是这几场签到题中最有意思的。

以下用 \(1\) 和 \(0\) 代表 \(T\) 和 \(F\)。

N=4

让目标一直在自己的白板上写 \(1\),其余人写 \(0\),每个人需要观察两个互不相同,且都不是自己的人,这样每个人就能够确定三个人是否为目标,从而推出剩下一个人是否为目标。

N=32

\(32=2^6\),看起来具有比较优秀的性质,考虑一个分治结构,对于当前分治区间 \([l,r](l\ne r)\),令 \(mid=\left\lfloor \frac{l+r}{2} \right\rfloor\),分为 \([l,mid]\) 和 \([mid+1,r]\) 两部分,我们想让 \([l,r]\) 内的所有人都知道目标在 \([l,mid]\) 中还是 \([mid+1,r]\) 中,或者目标不在 \([l,r]\) 中,首先递归两个区间,于是 \(l\) 知道了目标是否位于 \([l,mid]\) 中,\(r\) 知道了目标是否位于 \([mid+1,r]\) 中,若目标在 \([l,mid]\) 中,则让 \(l\) 在白板上写 \(1\),否则写 \(0\),若目标在 \([mid+1,r]\) 中,则让 \(r\) 在白板上写 \(1\),否则写 \(0\),\([l+1,r-1]\) 内的全部人都写 \(0\),对于 \([l,mid]\) 内所有人,让他们查看 \(r\) 白板上的符号,根据其确定目标是否在 \([mid+1,r]\) 中,同理 \([mid+1,r]\) 内所有人根据 \(l\) 白板上的内容判断目标是否在 \([l,mid]\) 中。

对整体进行一次分治后,每个人都知道目标位于某个长度为 \(2^k(0\le k\le 4)\) 的区间内,再根据这个分治结构,从上到下进行二分,即设此时每个人都已知目标在 \([l,r]\) 中,令 \(mid=\left\lfloor \frac{l+r}{2} \right\rfloor\),那么根据初始的分治结构,\(l\) 一定知道目标是否在 \([l,mid]\) 中,若目标在 \([l,mid]\) 中,让 \(l\) 在白板上写 \(1\),否则写 \(0\),然后让所有人查看 \(l\) 白板上的内容,就能够判断出目标在 \([l,mid]\) 中还是 \([mid+1,r]\) 中,继续该过程即可。

分治层数看似是 \(6\),实际上最下面一层的每个区间只有一个人,不需要考虑,而最上面一层(\([1,32]\))也不用考虑,因为第一次分治时只需要通过第二层(长度为 \(16\) 的层)往后的过程,就能让 \([1,16]\) 内的人知道目标是否在 \([1,16]\) 内,若不在则一定在 \([17,32]\) 内,不需要观察编号为 \(32\) 的人白板上的内容来确定,对于 \([17,32]\) 内的人也同理,所以实际有意义的分治层数为 \(4\),两个阶段只需要 \(4+4=8\) 个回合就能让所有人确定目标。

N=48

和 \(N=32\) 类似,注意到 \(48=3\times 2^4\)。

于是分成三个区间 \([1,16],[17,32],[33,48]\) 进行上面的第一次分治,这样每个人都知道了目标是否在自己所在区间,然后在目标所在区间的左端点白板上写上 \(1\),其余写 \(0\),每个人观察另外两个区间中某一个区间的左端点,那么每个人就知道了目标是否在其他一个区间内,从而推出目标是否在剩下的一个区间内,于是每个人都知道了目标在这三个区间中的哪一个,再使用与 \(N=32\) 相同的二分即可确定目标,需要回合数为 \(4+1+4=9\)。

9.22

[JOIST 2025] 勇者比太郎 3 / Bitaro the Brave 3

题意

有 \(N\) 只怪物,第 \(i\) 只怪物出现时间为 \(S_i\),强度 \(P_i\),初始生命值 \(H_i\)。

你在打防御战,共有 \(L\) 种等级,等级为 \(l\) 时怪物 \(i\) 生命值为 \(l\times H_i\),其余属性不变,防御战共持续 \(T\) 秒,每一秒你可以选择一个出现在场上且生命值不为 \(0\) 的怪物并对其攻击,使其生命值减 \(1\),令一局防御战后第 \(i\) 只怪物剩余血量为 \(h_i\),定义该局防御战的代价为 \(\sum\limits_{i=1}^{N}h_i\times P_i\)。

有 \(Q\) 次询问,每次询问给定一个阈值 \(M\),你需要设计攻击方案,使得代价不超过 \(M\) 的前提下,等级 \(l\) 最大,输出最大的 \(l\)(不存在则输出 \(0\)),保证每次询问 \(M\) 递增。

\(1\le N\le 6000,1\le L\le 10^7,1\le Q\le 10^6,1\le T\le 10^{18},1\le H_i,P_i,1\le \sum\limits_{i=1}^{N}H_i\times P_i \le 10^{11},0\le S_i<T\)。

solution

吓哭了呀。

考虑对每个 \(l\) 求出最小代价,每个询问双指针回答即可。

首先考虑将时间反转,即 \(S_i\leftarrow T-S_i\),这样就变成了全部怪物初始时出现,每只怪物在 \(S_i\) 时离开,这样更方便后面的推导。

我们规定 \(P_0=0,S_0=0\)。

首先考虑 \(P_i=1\) 时怎么做。

没了 \(P\) 的限制,我们可以直接对 \(S_i\) 排序,即优先处理截止时间较小的怪物,令 \(C_i=\sum\limits_{j=1}^{i} H_{j}\),那么可以得到最后的最小代价即为:

\[f(l)=\max(0,\max_{i=1}^{N}(C_i\times l-S_i)) \]

答案下界显然是这个式子,因为前 \(i\) 只怪总血量 \(C_i\times l\),只有 \(S_i\) 的时间解决它们,所以至少剩余 \(C_i\times l-S_i\)。

证明一下答案上界是这个式子,设该最大值在 \(i=k\) 时取得。

  • 对于 \(j>k\),有 \(S_j-S_k\ge l\times (C_j-C_k)\),所以 \(j\) 一定会被打完(生命值归零);
  • 对于 \(j<k\),由于投入到前 \(k\) 只怪物上的时间是一段前缀,且长度为 \(\min(l\times C_k,\min\limits_{m=1}^{k}(S_m+l\times (C_k-C_m)))\),由于 \(S_m+l\times (C_k-C_m)\ge S_k\),所以长度为 \(\min(l\times C_k,S_k)\),若 \(l\times C_k\ge S_k\),则剩余 \(l\times C_k - S_k=f(l)\),符合最大值,否则说明前 \(k\) 只全部被打完,值为 \(0\)。

那么容易得到 \(f\) 是由若干条直线 \(y=C_i x-S_i\) 组成的上包络,由于 \(H_i>0\),有 \(C_i\) 递增,斜率递增即可 \(O(N)\) 使用单调栈求出这个包络,然后对包络上每条直线对其覆盖范围进行更新,维护斜率和截距的差分后 \(O(L)\) 前缀和即可求出,注意由于 \(l\) 只取整数值,交点需要进行上取整。

然后扩展到 \(P_i\ne 1\) 的情况,按 \(P\) 从小到大排序,将代价的式子写成另一种形式。

\[\sum_{j=1}^{N} h_j\times P_j=\sum_{j=1}^{N} \sum_{P_i\le P_j} h_j\times (P_i-P_{i-1})= \sum_{i=1}^{N} (P_i-P_{i-1}) \times \sum_{P_j\ge P_i} h_j \]

枚举 \(i\),后面的式子就与 \(P\) 无关了,将所有 \(P_j\ge P_i\) 的 \(j\) 拿出来跑一遍 \(P=1\) 的情况,将当前层包络对答案的贡献乘上系数 \(P_{i}-P_{i-1}\),对所有 \(i\) 做完后再做前缀和即可得到答案。

不过 \(h_j\) 根据最终策略决定,但是枚举 \(i\) 每次单独计算得到的 \(h_j\) 不一定相同,这样上面的做法还正确吗?

可以证明一定存在一组 \(h_j\) 使得每轮 \(i\) 计算时,代价恰好达到 \(f_i(l)\)(该层最小代价)。

根据贪心,攻击怪物的方案应该是根据 \(P\) 从大到小,依次考虑每只怪物,并且对于一只怪物 \(i\),从 \(S_i\) 向前选取最晚未占用的 \(H_i\times l\) 的时间攻击它,根据这样的策略,从 \(f_i(l)\) 到 \(f_{i-1}(l)\) 时,第 \(j(j\ge i)\) 只怪物的攻击方案是已经确定的,所以 \(h_{j}\) 不会因为加入了 \(i-1\) 这只怪物而改变,同时,这组 \(h_j\) 就是根据最优贪心确定的结果,一定符合 \(f_i(l)\) 和 \(f_{i-1}(l)\) 的最优性,所以上面的做法是正确的。

总时间复杂度 \(O(N^2+L+Q)\)。

Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=6e3+10;
constexpr int M=1e7+10;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

inline void read(ll &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

struct Line{ll k,b;}d[M];
inline ll Line_corss(Line x,Line y) {
	ll u=y.b-x.b,d=x.k-y.k;
	if (d<0) u=-u,d=-d;
	return u/d+(u%d!=0);
}

int n,m,Len;
ll T,ans[M];
struct Node{ll s,h,p;}a[N];
inline bool cmp(Node x,Node y) {return x.s<y.s;}

inline void solve(ll lim,ll v) {
	if (!v) return;
	static Line st[N];
	int top=0;ll sum=0;
	st[++top]={0,0};
	f(i,1,n) {
		if (a[i].p<lim) continue;
		sum+=a[i].h;
		Line now={sum,-a[i].s};
		while (top>1&&Line_corss(st[top-1],st[top])>=Line_corss(st[top],now)) top--;
		st[++top]=now;
	}
	ll k=0,b=0;
	f(i,2,top) {
		ll x=Line_corss(st[i],st[i-1]);
		if (x>m) break;
		d[x].k+=(st[i].k-k)*v;
		d[x].b+=(st[i].b-b)*v;
		k=st[i].k,b=st[i].b;
	}
}

int main() {
	read(n);read(m);read(T);
	f(i,1,n) read(a[i].s),read(a[i].h),read(a[i].p),a[i].s=T-a[i].s;
	sort(a+1,a+1+n,cmp);
	static ll mp[N],Len;
	f(i,1,n) mp[++Len]=a[i].p;
	sort(mp+1,mp+1+Len);
	Len=unique(mp+1,mp+1+Len)-mp-1;
	f(i,1,Len) solve(mp[i],mp[i]-mp[i-1]);
	ll k=0,b=0;
	f(i,1,m) {
		k+=d[i].k,b+=d[i].b;
		ans[i]=k*i+b;
	}
	int q,now=0;read(q);
	while (q--) {
		ll d;read(d);
		while (now<m&&ans[now+1]<=d) now++;
		printf("%d\n",now);
	}
	return 0;
}

[JOIST 2025] 大会 / Conference

题意

给定一个长度为 \(n\),仅包含 \(\texttt{A}\)、\(\texttt{B}\)、\(\texttt{C}\) 和 \(\texttt{?}\) 且满足首尾均为 \(\texttt{A}\) 的字符串 \(S\),共有 \(q\) 次询问,每次询问给出三个参数 \(X,Y,Z\),你需要求出将 \(S\) 中 \(X\) 个 \(\texttt{?}\) 替换为 \(\texttt{A}\),\(Y\) 个 \(\texttt{?}\) 替换为 \(\texttt{B}\),\(Z\) 个 \(\texttt{?}\) 替换为 \(\texttt{C}\)(保证 \(X+Y+Z\) 恰为 \(S\) 中 \(\texttt{?}\) 的数量),使得满足 \(S_i \ne S_{i+1}(1\le i<N)\) 的下标 \(i\) 数量最小,求该数量的最小可能值,询问间独立。

\(2\le n\le 3\times 10^5,1\le q\le 2\times 10^5\)。

solution

难飞了。

对于每个询问,令 \(d_A=X,d_B=Y,d_C=Z,M=X+Y+Z\)。

对于一段连续的 \(\texttt{?}\) 进行考虑,考虑其两个端点,只有端点字母相同和不同两种情况,记为 \(X\) 型和 \(XY\) 型,分别表示两端均为字符 \(X\) 的段和一端为 \(X\),另一端为 \(Y\) 的段。

首先一些位置必然对答案造成贡献,这个贡献为 \(S_{i}\ne \texttt{?} \land S_{i+1} \ne \texttt{?} \land S_{i}\ne S_{i+1}\) 的数量加上 \(XY\) 型段的数量,这些贡献无论如何都不可能被消去,所以直接计入答案,那么每次询问需要考虑的是除去必要贡献后的增加量。

先不考虑替换数量的限制,考虑如何才能最小化 \(S_{i}\ne S_{i+1}\) 数量,显然对于 \(X\) 型段,中间的问号全部替换为 \(X\),对于 \(XY\) 型段,一段前缀替换为 \(X\),一段后缀替换为 \(Y\),这样的填法使得额外贡献为 \(0\)。

先明确一些基本事实:对于 \(X\) 型段,在其中填入两种字符(有一种字符为 \(X\))会造成 \(2\) 的额外贡献,填入三种字符会造成 \(3\) 的额外贡献,对于 \(XY\) 型段,在其中填入三种字符会造成 \(1\) 的额外贡献(一段前缀填 \(X\),一段后缀填 \(Y\),中间填 \(Z\))。

令 \(P(X)\) 为 \(X\) 型段长度总和,\(P(XY)\) 为 \(XY\) 型段长度总和。

对于每个字符 \(X\),可以求出最优情况(额外贡献为 \(0\))下至少填入数量 \(L_X=P(X)\) 和最多填入数量 \(R_X=P(X)+\sum\limits_{Y\ne X}P(XY)\),那么一组 \((d_A,d_B,d_C)\) 能够取到最优情况,当且仅当对于任意字符 \(X\) 均有 \(L_X\le d_X\le R_X\)。

首先若 \(d_X\) 不在 \([L_X,R_X]\) 中,则调整方案必然使得额外贡献增加,若 \(d_X\) 在 \([L_X,R_X]\) 内,构造一个二分图,左部点为各种字符,右部点为每个 \(\texttt{?}\) 连续段(容量为段长),字符 \(X\) 只能匹配到 \(X\) 型段或 \(XY\) 型段上,要求字符 \(X\) 恰好匹配 \(d_X\) 的单位容量,则根据 Hall 定理,对于单字符子集就是上面的上界限制,对于两个字符子集 \(X,Y\),另一个字符为 \(Z\),有限制 \(d_X+d_Y\le M- L_Z\),那么移项后恰为 \(L_Z\le d_Z\),即为下界限制,三个字符的子集是平凡的。

如果某个 \(d_X\) 不在 \([L_X,R_X]\) 中,我们需要对方案进行调整,于是根据 \(d_X>R_X\) 进行分类讨论,由于 \(\sum R_X\ge M = \sum d_X\),所以满足 \(d_X>R_X\) 的字符 \(X\) 的数量不超过两个,令这个数量为 \(num\)。

num=0

所有字符均满足上界限制,但是可能有字符不满足下界限制,并且最多只可能有一个字符不满足下界,否则可以推导出第三个字母会不满足上界,令不满足下界的字符为 \(X\),则有 \(d_X<L_X\),由于 \(L_X=P(X)\),所以对于这种情况的唯一处理方案就是在左右端点均为 \(X\) 的 \(\texttt{?}\) 连续段中填入其他字符,每修改一个段额外贡献会加 \(2\),需要修改的段长总量为 \(L_X-d_X\),根据长度从大到小替换即可,利用前缀和配合二分可以做到 \(O(\log n)\) 回答一次询问。

num=1

钦定不满足上界限制的字符为 \(A\),那么我们需要至少让 \(R_A\) 增加 \(d_A-R_A\)。

首先 \(B,C\) 可能不满足下界条件,若一个字符 \(X\) 不满足下界限制则像 \(num=0\) 时的处理一样按长度从大到小依次将 \(A\) 填入 \(X\) 型段中,\(R_A\) 增加量即为段长,这样做显然是不劣的。

若处理完下界后仍有 \(R_A<d_A\),那么共有三种替换方法(含有 \(A\) 的段并不能使 \(R_A\) 增加):

  • 在 \(B\) 型段中间放入 \(A\),代价加 \(2\);
  • 在 \(C\) 型段中间放入 \(A\),代价加 \(2\);
  • 在 \(BC\) 型段中间放入 \(A\),代价加 \(1\)。

这三种方法能给 \(R_A\) 带来的增加量均为其段长,这不是我们的 sale 吗,贪心地根据性价比(段长与代价增加量之比)从大到小选择物品(即为此题中的段),直到 \(R_A\) 不小于 \(d_A\),这样贪心得到的代价最多比最优代价多 \(1\)。

由于将用性价比更低且代价为 \(2\) 的物品替换当前选择的物品一定不优,所以得到代价比最优代价多 \(1\) 的情况只有以下两种:

  • 去掉前面某一个代价为 \(1\) 的物品,\(R_A\) 仍然可以不小于 \(d_A\);
  • 用后面某一个代价为 \(1\) 的物品替换当前选择的一个代价为 \(2\) 的物品,\(R_A\) 仍然可以不小于 \(d_A\)。

于是我们可以使用线段树进行维护,根据性价比排序后选择一段前缀使得 \(R_A\ge d_A\),这段前缀可以二分出来,然后考虑选中代价为 \(1\) 和 \(2\) 的段长最小值分别为多少,以及未选中的代价为 \(1\) 的段长最大值,可以二分求出来或者在线段树上直接维护,单组询问可以做到 \(O(\log n)\) 回答。

注意在处理下界时用过的段不能作为一个物品参与后续的选择,由于用过的段是按段长从大到小排序后的一段前缀,所以维护起来并不困难。

num=2

钦定满足上界限制的字符为 \(A\),那么需要让 \(R_B\) 增加 \(cap_B=d_B-R_B\),\(R_C\) 增加 \(cap_C=d_C-R_C\),那么有:

\[cap_B+cap_C=d_B+d_C-(P(B)+P(C)+P(AB)+P(AC)+2\times P(BC)) \]

即:

\[cap_B+cap_C=(M-d_A)-(M-P(A)+P(BC))=L_A-d_A-P(BC) \]

由于 \(cap_B+cap_C+P(BC)>0\),所以可以得到 \(d_A<L_A\),即字符 \(A\) 必然不满足下界条件,并且 \(L_A-d_A\ge cap_B+cap_C\),所以通过调整 \(A\) 的下界,可以做到将 \(B\) 和 \(C\) 的上界调整至合法。

调整下界的方法和之前一样,依次选择最长的段进行填入其他字符,令调整的段数为 \(k\),那么额外贡献下界为 \(2\times k\)。

从小到大考虑需要调整的段,先填入字符 \(B\),直到 \(d_B-R_B\) 不超过段长,在当前段调整完 \(B\) 后立刻调整 \(C\),剩下的段全部用于调整 \(C\),那么只有一段含有三种字符,所以额外贡献上界为 \(2\times k+1\)。

额外贡献达到下界,要求没有任何一个调整段填入了三种字符,所以现在问题变成了:对于所有调整段,令段长和为 \(T\),是否能够选出一个调整段的集合,使得其段长和 \(w\) 满足 \(cap_B\le w\le T-cap_C\),即让这个集合内的调整段填入字符 \(B\),其余段填入字符 \(C\),使得 \(B\) 和 \(C\) 的上界均满足条件。

这个问题看起来只能用背包解决,朴素的 bitset 优化背包可以做到 \(O(\frac{n^2}{\omega })\),但是无法通过。

考虑单调队列优化多重背包,令 \(f_i\) 为选出一个段长和为 \(i\) 的子集至少需要多少个段(即最短前缀的段数),对于一个段长 \(v\),段数为 \(c\),令段长大于 \(v\) 的段数为 \(sum\),那么有转移 \(f_{i}= \min\limits_{\frac{(i-j)}{v}\le c\land j<i\land i\equiv j \pmod{v}}(f_i,\max(f_j,sum+\frac{(i-j)}{v}))\),每次转移只需要考虑之前没有被覆盖过的 \(f_i\) 即可,由于若 \(j\) 可以被 \(v\) 之前的前缀表示出来,则必然有 \(f_j<sum+\frac{(i-j)}{v}\),所以可以根据 \(\bmod v\) 的值分成若干个转移链,在每条链上从小到大更新,并维护之前最大能够表示出来的 \(j\) 进行转移即可,这样对于每个段长 \(v\) 需要 \(O(n)\) 时间复杂度就可以完成转移。

由于段长和至多为 \(n\),所以不同的段长数量为 \(O(\sqrt n)\),整个转移就是 \(O(n\sqrt n)\) 的。

对于每个字符预处理出对应的 \(f\) 值,并做 RMQ(或线段树),每次询问 \([cap_B,T-cap_C]\) 内 \(f\) 最小值,判断其是否不超过 \(k\) 即可知道额外贡献是否可以达到下界,单组询问 \(O(\log n)\)。

于是这个题就做完了,总时间复杂度 \(O(n\sqrt n+q\log n)\)。

Code
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=3e5+10;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int n;
char ch[N];
int a[N],L[4],R[4];
vector<int> vec[4][4],svec[4];

inline int find(vector<int> &tmp,int x) {
	int l=0,r=tmp.size()-1,res=0;
	while (l<=r) {
		int mid=l+r>>1;
		if (tmp[mid]>=x) res=mid,r=mid-1;
		else l=mid+1;
	}
	return res+1;
}

inline int solve1(int x,int y,int z) {
	int ret=0;
	if (x<L[1]) ret+=find(svec[1],L[1]-x);
	if (y<L[2]) ret+=find(svec[2],L[2]-y);
	if (z<L[3]) ret+=find(svec[3],L[3]-z);
	return ret<<1;
}

struct Node {int v,w,id;};
inline bool cmp(Node x,Node y) {return x.v*2/x.w>y.v*2/y.w;}

struct Segment_Tree1 {
	int Len,c1,c2;
	int mn1[N<<2],mx1[N<<2],mn2[N<<2],s[N<<2],t[N<<2];
	int R1[N<<2],R2[N<<2],s1[N<<2],t1[N<<2],s2[N<<2],t2[N<<2];
	inline int ls(int x) {return x<<1;}
	inline int rs(int x) {return x<<1|1;}
	
	inline void build(int l,int r,int now,vector<Node> &tmp) {
		if (l==r) {
			Node k=tmp[l-1];
			if (k.w==1) mx1[now]=mn1[now]=k.v,mn2[now]=inf;
			else mn2[now]=k.v,mn1[now]=inf;
			s[now]=k.v;t[now]=k.w;
			if (k.id==1) {
				R1[now]=++c1;
				s1[now]=s[now];
				t1[now]=t[now];
			}
			if (k.id==2) {
				R2[now]=++c2;
				s2[now]=s[now];
				t2[now]=t[now];
			}
			return;
		}
		int mid=l+r>>1;
		build(l,mid,ls(now),tmp);
		build(mid+1,r,rs(now),tmp);
		mn1[now]=min(mn1[ls(now)],mn1[rs(now)]);
		mx1[now]=max(mx1[ls(now)],mx1[rs(now)]);
		mn2[now]=min(mn2[ls(now)],mn2[rs(now)]);
		s[now]=s[ls(now)]+s[rs(now)];
		t[now]=t[ls(now)]+t[rs(now)];
		R1[now]=max(R1[ls(now)],R1[rs(now)]);
		R2[now]=max(R2[ls(now)],R2[rs(now)]);
		s1[now]=s1[ls(now)]+s1[rs(now)];
		s2[now]=s2[ls(now)]+s2[rs(now)];
		t1[now]=t1[ls(now)]+t1[rs(now)];
		t2[now]=t2[ls(now)]+t2[rs(now)];
	}
	
	inline void init(vector<Node> &tmp) {
		if(tmp.empty())return;
		Len=tmp.size();
		build(1,Len,1,tmp);
	}
	
	inline int query(int l,int r,int now,int k,int MN1,int MN2,int MX1,int ret,int p1,int p2,int lst_s1,int lst_s2,int lst_t1,int lst_t2) {
		if (l==r) {
			MN1=min(MN1,mn1[now]);
			MN2=min(MN2,mn2[now]);
			k-=s[now];ret+=t[now];
			if (k+MN1<=0) return ret-1;
			if (k+MN2-MX1<=0) return ret-1;
			return ret;
		}
		int mid=l+r>>1,val_s=s[ls(now)],val_t=t[ls(now)];
		if (R1[ls(now)]<=p1) val_s-=s1[ls(now)],val_t-=t1[ls(now)];
		else val_s-=lst_s1,val_t-=lst_t1;
		if (R2[ls(now)]<=p2) val_s-=s2[ls(now)],val_t-=t2[ls(now)];
		else val_s-=lst_s2,val_t-=lst_t2;
		if (val_s>=k) return query(l,mid,ls(now),k,MN1,MN2,max(MX1,mx1[rs(now)]),ret,p1,p2,min(lst_s1,s1[ls(now)]),min(lst_s2,s2[ls(now)]),min(lst_t1,t1[ls(now)]),min(lst_t2,t2[ls(now)]));
		return query(mid+1,r,rs(now),k-val_s,min(MN1,mn1[ls(now)]),min(MN2,mn2[ls(now)]),MX1,ret+val_t,p1,p2,max(0,lst_s1-s1[ls(now)]),max(0,lst_s2-s2[ls(now)]),max(0,lst_t1-t1[ls(now)]),max(0,lst_t2-t2[ls(now)]));
	}
}tr1[4];

inline int solve2(int x,int y,int z) {
	int ret=0,cnt=0,pos1=0,pos2=0,pos3=0,s1=0,s2=0,s3=0;
	if (x<L[1]) {
		pos1=find(svec[1],L[1]-x);
		ret+=pos1*2,cnt+=(s1=svec[1][pos1-1]);
	}
	if (y<L[2]) {
		pos2=find(svec[2],L[2]-y);
		ret+=pos2*2,cnt+=(s2=svec[2][pos2-1]);
	}
	if (z<L[3]) {
		pos3=find(svec[3],L[3]-z);
		ret+=pos3*2,cnt+=(s3=svec[3][pos3-1]);
	}
	if (x>R[1]) {
		x-=cnt;if (x<=R[1]) return ret;
		return ret+tr1[1].query(1,tr1[1].Len,1,x-R[1],inf,inf,0,0,pos2,pos3,s2,s3,pos2*2,pos3*2);
	}
	if (y>R[2]) {
		y-=cnt;if (y<=R[2]) return ret;
		return ret+tr1[2].query(1,tr1[2].Len,1,y-R[2],inf,inf,0,0,pos1,pos3,s1,s3,pos1*2,pos3*2);
	}
	if (z>R[3]) {
		z-=cnt;if (z<=R[3]) return ret;
		return ret+tr1[3].query(1,tr1[3].Len,1,z-R[3],inf,inf,0,0,pos1,pos2,s1,s2,pos1*2,pos2*2);
	}
	puts("Error_solve2");
}

struct Segment_Tree2 {
	int mn[N<<2],dp[N];
	inline int ls(int x) {return x<<1;}
	inline int rs(int x) {return x<<1|1;}
	
	inline void build(int l,int r,int now) {
		if (l==r) return mn[now]=dp[l],void();
		int mid=l+r>>1;
		build(l,mid,ls(now));
		build(mid+1,r,rs(now));
		mn[now]=min(mn[ls(now)],mn[rs(now)]);
	}
	
	inline int query(int x,int y,int l,int r,int now) {
		if (x<=l&&y>=r) return mn[now];
		int mid=l+r>>1,res=inf;
		if (x<=mid) res=query(x,y,l,mid,ls(now));
		if (y>mid) res=min(res,query(x,y,mid+1,r,rs(now)));
		return res;
	}
	
	int t[N],sum;
	inline void init_dp(vector<int> &tmp) {
		for (int x:tmp) t[x]++;
		f(i,1,n) dp[i]=inf;
		F(i,n,1) {
			if (!t[i]) continue;
			const int cnt=t[i],Len=i-1;
			f(c,0,Len) {
				int lst=-1;
				for (int j=c;j<=n;j+=i) {
					if (dp[j]!=inf) lst=j;
					if (lst!=-1&&dp[j]==inf&&(j-lst)/i<=cnt) dp[j]=sum+(j-lst)/i;
				}
			}
			sum+=t[i];
		}
		build(0,n,1);
	}
}tr2[4];

inline int solve3(int x,int y,int z) {
	int op=((x<L[1])?1:((y<L[2])?2:3)),c1=0,c2=0;
	int k=find(svec[op],max({L[1]-x,L[2]-y,L[3]-z})),sum=svec[op][k-1];
	if (op==1) c1=y-R[2],c2=z-R[3];
	if (op==2) c1=x-R[1],c2=z-R[3];
	if (op==3) c1=x-R[1],c2=y-R[2];
	if (tr2[op].query(c1,sum-c2,0,n,1)<=k) return k<<1;
	return k<<1|1;
}

inline void init() {
	f(i,1,3) {
		for (int x:vec[i][i]) svec[i].push_back(x);
		for (int k=1,L=svec[i].size();k<L;k++) svec[i][k]+=svec[i][k-1];
		tr2[i].init_dp(vec[i][i]);
	}
	f(i,1,3) {
		vector<Node> tmp;int now=0;
		f(j,1,3) if (i^j) {
			now++;
			for (int x:vec[j][j]) tmp.push_back({x,2,now});
			f(k,j+1,3) if (k^i) for (int x:vec[j][k]) tmp.push_back({x,1,0});
		}
		sort(tmp.begin(),tmp.end(),cmp);
		tr1[i].init(tmp);
	}
	f(i,1,3) f(j,i,3) {
		if (i==j) {for (int x:vec[i][j]) L[i]+=x,R[i]+=x;}
		else {for (int x:vec[i][j]) R[i]+=x,R[j]+=x;}
	}
}

int main() {
	read(n);
	scanf("%s",ch+1);
	f(i,1,n) a[i]=(ch[i]=='?'?0:ch[i]-'A'+1);
	int num=0,lst=0,sum=0;
	f(i,1,n) {
		sum+=((i!=n)&&(a[i]!=0)&&(a[i+1]!=0)&&(a[i]!=a[i+1]));
		if (!a[i]) {
			if (!num) lst=a[i-1];
			num++;
		}
		else if (num) {
			vec[min(lst,a[i])][max(lst,a[i])].push_back(num);
			num=0;
		}
	}
	f(i,1,3) f(j,i,3) {
		sort(vec[i][j].begin(),vec[i][j].end(),[](int x,int y){return x>y;});
		sum+=(i!=j)*vec[i][j].size();
	}
	int q;read(q);init();
	while (q--) {
		int x,y,z;read(x);read(y);read(z);
		int num=(x>R[1])+(y>R[2])+(z>R[3]);
		if (num==0) printf("%d\n",sum+solve1(x,y,z));
		if (num==1) printf("%d\n",sum+solve2(x,y,z));
		if (num==2) printf("%d\n",sum+solve3(x,y,z));
	}
	return 0;
}

9.23

[JOIST 2025] 迁移计划 / Migration Plan

solution

最签到的一题。

由于操作 \(1\) 是对整个层整体操作,所以考虑直接维护每层节点的值,合并起来看起来可以均摊复杂度,不过将子树中有值的位置移到父亲没值的位置复杂度就假了,所以考虑每层开一棵线段树维护 dfn 序从而确定一个节点子树合并上来的权值,那么线段树合并即可。

操作二是平凡地在线段树上单点加,操作三则是在求 dfn 在子树内的和。

时间复杂度 \(O(n\log n)\)。

Code
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=2e6+10;
constexpr int P=6e7;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int n,m,dfn_idx,Tree_tot;
int f[N],a[N],rt[N];
int dep[N],siz[N],dfn[N];
vector<int> e[N];

struct Segment_Tree {
	struct Tree_Node {int ls,rs,sum;}tr[P];
	#define ls(x) tr[x].ls
	#define rs(x) tr[x].rs
	
	inline void update(int &now,int x,int l,int r,int k) {
		if (!now) now=++Tree_tot;
		if (l==r) return tr[now].sum+=k,void();
		int mid=l+r>>1;
		if (x<=mid) update(ls(now),x,l,mid,k);
		else update(rs(now),x,mid+1,r,k);
		tr[now].sum=tr[ls(now)].sum+tr[rs(now)].sum;
	}
	
	inline int merge(int x,int y,int l,int r) {
		if (!x||!y) return x|y;
		if (l==r) return tr[x].sum+=tr[y].sum,x;
		int mid=l+r>>1;
		ls(x)=merge(ls(x),ls(y),l,mid);
		rs(x)=merge(rs(x),rs(y),mid+1,r);
		tr[x].sum=tr[ls(x)].sum+tr[rs(x)].sum;
		return x;
	}
	
	inline int query(int x,int y,int l,int r,int now) {
		if (!now) return 0;
		if (x<=l&&y>=r) return tr[now].sum;
		int mid=l+r>>1,res=0;
		if (x<=mid) res=query(x,y,l,mid,ls(now));
		if (y>mid) res+=query(x,y,mid+1,r,rs(now));
		return res;
	}
}tr;

inline void dfs(int now) {
	siz[now]=1;
	dfn[now]=++dfn_idx;
	tr.update(rt[dep[now]],dfn[now],1,n,a[now]);
	for (int v:e[now]) {
		dep[v]=dep[now]+1;
		dfs(v);
		siz[now]+=siz[v];
	}
}

int main() {
	read(n);
	f(i,2,n) read(f[i]),e[f[i]].push_back(i);
	f(i,1,n) read(a[i]);
	dfs(1);read(m);
	while (m--) {
		int op,x,y;
		read(op);read(x);
		if (op==1) {
			read(y);
			rt[y]=tr.merge(rt[x],rt[y],1,n);
			rt[x]=0;
		}
		if (op==2) {
			read(y);
			tr.update(rt[dep[x]],dfn[x],1,n,y);
		}
		if (op==3) printf("%d\n",tr.query(dfn[x],dfn[x]+siz[x]-1,1,n,rt[dep[x]]));
	}
	return 0;
}

[JOIST 2025] 电路 2 / Circuit 2

solution

欸欸欸怎么调不出来啊,诶诶怎么没时间了啊,\(66\) 分遗憾离场,距离满分一步之遥。

不难发现开关形成了一个二叉树结构,叶子即开关 \(N\sim 2N\),根为开关 \(0\),令 \(v_i\) 表示开关 \(i\) 是否开启,一个叶子节点 \(i\) 返回值即为 \(v_i\),非叶子节点 \(i\) 的返回值即为其左右子节点返回值经过组件槽 \(i\) 运算后再异或上 \(v_i\) 得到的值,最终的返回值即为根节点返回值。

我们希望做的事情是:将某些节点作为待测节点,若这些节点中存在组件为 \(\texttt{or}\) 的节点则让根节点返回 \(1\),否则返回 \(0\)。

Subtask \(5\)

仅考虑非叶子节点,它们构成了一条链,对于一段前缀 \([0,i]\),将 \(j\in[0,i]\) 的节点 \(j\) 右子节点(即输入的 \(V_j\))设为开启,其他节点全部关闭,发现若 \([0,i]\) 中有一个组件槽为 \(\texttt{or}\) 则根节点返回 \(1\),否则返回 \(0\),于是一个显然的思路就是二分,每次找到第一个组件槽为 \(\texttt{or}\) 的节点 \(j\),并在后面设置前缀时将其右子节点设为关闭即可,需要次数约为 \(R\log_2 N\),可以通过 Subtask \(4\)。

考虑优化一下,直接二分范围太大了,所以不妨考虑分块,每块块长为 \(B\),每次检查一个块内有没有尚未找到的组件槽为 \(\texttt{or}\) 的节点,若有则在块内二分找到其中一个并标记,直到块内所有 \(\texttt{or}\) 均被找到,这样需要的次数约为 \(\frac{N}{B}+R(\log_2 B+1)\),取 \(B=32\) 可以通过,注意检查一个块时需要将上方的开关设置成只用于传递信号的状态(不会自己产生信号,但下面的信号可以传至根节点)。

Subtask \(7\)

这是一棵完全二叉树,发现它的深度很低,不妨从深度下手。

我们发现如果用 \(1\) 作为有 \(\texttt{or}\) 出现的信号则组件槽为 \(\texttt{and}\) 的节点难以传递子节点的信号,于是考虑设计新的信号,若当前组件槽为 \(\texttt{and}\),则若一个子节点传入 \(0\) 信号说明当前有 \(\texttt{or}\) 出现,若当前节点组件槽为 \(\texttt{or}\),则若一个子节点传入 \(1\) 信号说明当前有 \(\texttt{or}\) 出现,由于对组件 \(\texttt{and}\) 传入 \(0\) 必然返回 \(0\),所以信号不会因为另一个子树传入的信息而消失,\(\texttt{or}\) 同理,根据父节点需要的信号来设置当前节点的 \(v\) 值,不难发现这样能够顺利地将信号传递至根节点。

由于同一层的子树两两不交,不会相互影响测试,且测试时需要父节点传递信息,所以考虑由浅到深逐层确定其中的组件槽 \(\texttt{or}\),对于一个非叶子节点,将其左子树内叶子节点全设为关闭,右子树叶子节点全设为开启,子树内非叶子节点(除了这个节点本身)全部设为关闭,若这个节点组件槽为 \(\texttt{or}\) 则会得到 \(1\) 的信号,否则会得到 \(0\) 的信号,浅于这层的开关要设置成只用于传递信号的状态,根据父节点的需要设置当前节点的 \(v\) 值即可将这个信号传递至根节点。

然后和 Subtask \(5\) 一样对当前层节点分块做二分来找到使根输出变为 \(1\) 的(即组件为 \(\texttt{or}\) 的)节点,次数约为 \(\frac{N}{B}+R(\log_2 B+1)+\log_2 N\),取 \(B=32\) 可以通过。

正解

其实打完以上两个 Subtask 就已经很接近正解了。

找到当前最长的一条自上而下由组件尚未确定的节点组成的链,根据其长度进行讨论。

  • 若这条链长度不小于 \(B\),则用 Subtask \(5\) 的方法处理掉这条链,区别在于上面的是对右边节点设为开启,这里是对链上节点的链外子树设为开启;
  • 若这条链长度小于 \(B\),则使用 Subtask \(7\) 的方法扩展,由于这是最长的未确认的链,所以扩展的次数不会超过 \(B\),同时最长链长度不增,所以后面一定一直使用这种扩展方式。

总次数约为 \(\frac{N}{B}+R(\log_2 B+1)+B\),取 \(B=32\) 可以通过,取 \(B=64\) 次数更少。

Code
#include "circuit.h"
#include<cstdio>
#include<algorithm>
#include<string>
#include<vector>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=16000+10;
constexpr int B=64;

int n;
int v[N],ls[N],rs[N],f[N];
int vis[N],ans[N],mxd[N];

//int query(std::string s);
inline int ask() {
	string s;
	f(i,0,n*2) s+=(v[i]+'0');
	return query(s);
}

inline void fill_col(int now,int c) {
	if (!ls[now]&&!rs[now]) return v[now]=c,void();
	v[now]=0;
	fill_col(ls[now],c);
	fill_col(rs[now],c);
}

inline void dfs_fa(int now) {
	if (!ls[now]&&!rs[now]) return;
	f[ls[now]]=f[rs[now]]=now;
	dfs_fa(ls[now]);
	dfs_fa(rs[now]);
}

inline void setin(int L,int R,vector<int> &tmp) {
	f(i,L,R) if (!ans[tmp[i]]) {
		fill_col(ls[tmp[i]],0);
		fill_col(rs[tmp[i]],1);
		v[tmp[i]]=ans[f[tmp[i]]]^1;
	}
}

inline void reset(int L,int R,vector<int> &tmp) {
	f(i,L,R) {
		int now_col=v[tmp[i]];
		fill_col(tmp[i],1^v[tmp[i]]^ans[f[tmp[i]]]);
		v[tmp[i]]=now_col;
	}
}

inline void solve_dep(vector<int> tmp) {
	reset(0,tmp.size()-1,tmp);
	for (int L=0,mxLen=tmp.size()-1,R=min(L+B-1,mxLen);L<=mxLen;L+=B,R=min(L+B-1,mxLen)) {
		setin(L,R,tmp);
		int flag=ask(),lst=L;
		reset(L,R,tmp);
		while (flag) {
			int l=lst,r=R,res=-1;
			while (l<=r) {
				int mid=l+r>>1;
				setin(l,mid,tmp);
				int ret=ask();
				reset(l,mid,tmp);
				if (ret) res=mid,r=mid-1;
				else l=mid+1;
			}
			ans[tmp[res]]=1;
			lst=res+1;
			setin(L,R,tmp);
			flag=ask();
			reset(L,R,tmp);
		}
	}
	for (int x:tmp) v[x]=ans[x]^ans[f[x]],vis[x]=1;
	reset(0,tmp.size()-1,tmp);
}

inline void solve_len(vector<int> tmp) {
	fill_col(tmp.front(),0);
	for (int L=0,mxLen=tmp.size()-1,R=min(L+B-1,mxLen);L<=mxLen;L+=B,R=min(L+B-1,mxLen)) {
		v[tmp[L]]=ans[f[tmp[L]]]^1;
		f(i,L,R) fill_col(rs[tmp[i]],1);
		int flag=ask(),lst=L;
		f(i,L,R) fill_col(rs[tmp[i]],0);
		while (flag) {
			int l=lst,r=R,res=-1;
			while (l<=r) {
				int mid=l+r>>1;
				f(i,L,mid) if (!ans[tmp[i]]) fill_col(rs[tmp[i]],1);
				int ret=ask();
				f(i,L,mid) if (!ans[tmp[i]]) fill_col(rs[tmp[i]],0);
				if (ret) res=mid,r=mid-1;
				else l=mid+1;
			}
			ans[tmp[res]]=1;
			lst=res+1;
			f(i,L,R) if (!ans[tmp[i]]) fill_col(rs[tmp[i]],1);
			flag=ask();
			f(i,L,R) if (!ans[tmp[i]]) fill_col(rs[tmp[i]],0);
		}
		f(i,L,R) {
			v[tmp[i]]=ans[tmp[i]]^ans[f[tmp[i]]];
			v[rs[tmp[i]]]=ans[tmp[i]]^1;
			vis[tmp[i]]=1;
		}
	}
	fill_col(ls[tmp.back()],ans[tmp.back()]^1);
}

vector<int> tmp;
inline void dfs1(int now) {
	if (!ls[now]&&!rs[now]) return;
	if (!vis[now]) {
		tmp.push_back(now);
		return;
	}
	dfs1(ls[now]);dfs1(rs[now]);
}

inline void dfs2(int now) {
	mxd[now]=1;
	if (!ls[now]&&!rs[now]) return;
	dfs2(ls[now]);dfs2(rs[now]);
	mxd[now]=max(mxd[ls[now]],mxd[rs[now]])+1;
}

inline void dfs3(int now) {
	if (!ls[now]&&!rs[now]) return;
	tmp.push_back(now);
	if (mxd[ls[now]]<mxd[rs[now]]) swap(ls[now],rs[now]);
	dfs3(ls[now]);
}

inline bool solve_all() {
	tmp.clear();dfs1(0);
	if (tmp.empty()) return 0;
	vector<int> now=tmp,mx;
	for (int x:now) {
		tmp.clear();
		dfs2(x);dfs3(x);
		if (tmp.size()>mx.size()) mx=tmp;
	}
	if (mx.size()>=B) solve_len(mx);
	else solve_dep(now);
	return 1;
}

std::string solve(int _N, int R, std::vector<int> U, std::vector<int> V) {
	n=_N;
	f(i,0,n-1) ls[i]=U[i],rs[i]=V[i];
	fill_col(ls[0],0);
	fill_col(rs[0],1);
	ans[0]=ask();v[0]=ans[0]^1;vis[0]=1;
	fill_col(ls[0],ans[0]^1);
	fill_col(rs[0],ans[0]^1);
	dfs_fa(0);
	while (solve_all());
	string ret;
	f(i,0,n-1) ret+=(ans[i]?'|':'&');
	return ret;
}

[JOIST 2025] 救护车 / Ambulance

题意

有一个 \(L\times L\) 的网格,网格四个角落各有一家医院,每家医院有一辆救护车,现在有 \(N\) 个病人需要运送到医院,每辆救护车从其对应医院出发,移动到病人位置将其接上车,再运回其对应医院放下病人,每个时刻只能移动到相邻格子且最多运送一名病人,并且只能将病人运送到其对应的医院,你需要判断能否安排救护车的运送方案使得所有病人在时刻 \(T\) 前均被送到医院。

\(1\le N\le 160,3\le L\le 10^4,1\le T\le 2\times 10^4\)。

solution

先给医院编号,第一家医院位于 \((1,1)\),第二家位于 \((1,L)\),第三家位于 \((L,1)\),第四家位于 \((L,L)\),由于运送时间为运送距离的两倍,将时间限制 \(T\leftarrow \left\lfloor \frac{T}{2}\right\rfloor\),那么对于每家医院的要求就是运送到这家医院的病人到医院的距离和不超过 \(T\)。

考虑只有第一家和第四家医院的情况,将一个病人 \((x,y)\) 运送到第一家医院距离为 \(x+y-2\),运送到第四家医院距离为 \(2L-(x+y)\),那么按照 \(x+y\) 排序,运送到第一家医院的病人一定是一个前缀,运送到第四家医院的病人一定是一个后缀,否则若存在 \(i>j\) 且 \(i\) 运送到第一家医院,\(j\) 运送到第四家医院,由于 \(x_i+y_i\ge x_j+y_j\),所以将 \(i\) 运送到第四家医院,\(j\) 运送到第一家医院,两家医院的距离和均不增,一定不劣于原方案。

再考虑变成四家医院的情况,依然按照 \(x+y\) 排序并枚举分界点,分界点前面的病人去第一家,第二家或第三家医院,分界点后面的病人去第二家,第三家或第四家医院,再考虑对于分界点前面和后面同样枚举去第二家医院和第三家医院的分界点,以前半部分为例,对于这些病人按照到第二家医院的距离升序排序,那么一个前缀的病人去往第一家或第二家医院,后缀的病人去往第一家或第三家医院一定不劣,证明与上面相同。

令第一部分只能去往第一家或第二家医院,第二部分只能去往第一家或第三家医院,第三部分只能去往第二家或第四家医院,第四部分只能去往第三家或第四家医院。

那么我们需要判定枚举完分界点后是否存在一个安排方案合法,考虑 dp,令:

  • \(f_{1,i,j}\) 表示第一部分从前向后考虑到第 \(i\) 个病人第一家医院的距离和不超过 \(j\),第二家医院的距离和最小值;
  • \(f_{2,i,j}\) 表示第二部分从后向前考虑到第 \(i\) 个病人第一家医院的距离和不超过 \(j\),第三家医院的距离和最小值;
  • \(f_{3,i,j}\) 表示第三部分从前向后考虑到第 \(i\) 个病人第二家医院的距离和不超过 \(j\),第四家医院的距离和最小值;
  • \(f_{4,i,j}\) 表示第四部分从后向前考虑到第 \(i\) 个病人第三家医院的距离和不超过 \(j\),第四家医院的距离和最小值。

转移是简单的,以 \(f_1\) 为例,考虑加入第 \(i\) 个病人,令 \(t_1\) 为当前病人到第一家医院的距离,\(t_2\) 为到第二家医院的距离,那么有转移:\(f_{1,i,j}\leftarrow \min(f_{1,i-1,j}+t_2,f_{1,i-1,j-t_1})\)。

令 \(l_i\) 为第 \(i\) 部分病人数量,枚举第一部分第一家医院使用的距离和 \(t_1\),那么第一家医院在第二部分最多能够承担 \(T-t_1\) 的距离和,第二家医院在第一部分使用的距离和为 \(t_2=f_{1,l_1,t_1}\),第三家医院在第二部分使用的距离和为 \(t_3=f_{2,l_2,T-t_1}\),第四家医院的总距离和为 \(t_4=f_{3,l_3,T-t_2}+f_{4,l_4,T-t_3}\),合法条件即为 \(t_1,t_2,t_3,t_4\le T\)。

那么我们枚举三个分界点时间复杂度为 \(O(n^3)\),每次进行 dp 复杂度为 \(O(nT)\),所以总复杂度为 \(O(n^4T)\),完全不可接受。

考虑优化,首先还是枚举第一个分界点,然后考虑对所有病人按照到第二家医院的距离排序,对所有病人枚举第二个分界点,那么对于一个病人,若他在第一次排序时在第一个分界点前,则他可以去第一家医院,否则他可以去第四家医院,若他在第二次排序时在第二个分界点前,则他可以去第二家医院,否则他可以去第三家医院,这样的策略一定不劣,因为一个病人到第二家医院和到第三家医院的距离和为定值,且交换一个去第二家医院的病人和去第三家医院的病人不会对第一家和第四家医院产生影响,故存在最优方案使去第二家的病人恰好构成按到第二家医院距离排序的全局前缀,即左右两侧的第二分界点由同一个全局分界点同时确定,所以枚举分界点的复杂度就变为 \(O(n^2)\) 了。

dp 时其实只关心第一个分界点,对于第一个分界点分成两部分分别按照到第二家医院的距离排序后,对于两个部分分别对每个前后缀求出对应的 \(f\) 值,枚举第二个分界点后取出对应的前后缀并枚举 \(t_1\) 进行判定即可,枚举第一个分界点时间复杂度为 \(O(n)\),每次 dp 时间复杂度 \(O(nT)\),所以 dp 部分总时间复杂度 \(O(n^2T)\),每次枚举两个分界点后枚举 \(t_1\) 进行判定时间复杂度 \(O(T)\),所以枚举判定部分总时间复杂度 \(O(n^2T)\)。

总时间复杂度 \(O(n^2T)\)。

Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=160+10;
constexpr int V=1e4+10;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int n,L,T,flag_ans;
int f1[N][V],f2[N][V],f3[N][V],f4[N][V];
struct Point{int x,y;}p[N],p1[N],p2[N];

inline int dis1(Point p) {return p.x+p.y-2;}
inline int dis2(Point p) {return L-1+p.x-p.y;}
inline int dis3(Point p) {return L-1-p.x+p.y;}
inline int dis4(Point p) {return L*2-p.x-p.y;}

inline bool cmp1(Point a,Point b) {return dis1(a)<dis1(b);}
inline bool cmp2(Point a,Point b) {return dis2(a)<dis2(b);}

inline void get_dp(int pos) {
	f(j,0,T) f1[0][j]=f2[pos+1][j]=f3[pos][j]=f4[n+1][j]=0;
	f(i,1,pos) {
		int t1=dis1(p[i]),t2=dis2(p[i]),Rmx=min(T,t1-1);
		f(j,0,Rmx) f1[i][j]=f1[i-1][j]+t2;
		f(j,t1,T) f1[i][j]=min(f1[i-1][j]+t2,f1[i-1][j-t1]);
	}
	F(i,pos,1) {
		int t1=dis1(p[i]),t2=dis3(p[i]),Rmx=min(T,t1-1);
		f(j,0,Rmx) f2[i][j]=f2[i+1][j]+t2;
		f(j,t1,T) f2[i][j]=min(f2[i+1][j]+t2,f2[i+1][j-t1]);
	}
	f(i,pos+1,n) {
		int t1=dis2(p[i]),t2=dis4(p[i]),Rmx=min(T,t1-1);
		f(j,0,Rmx) f3[i][j]=f3[i-1][j]+t2;
		f(j,t1,T) f3[i][j]=min(f3[i-1][j]+t2,f3[i-1][j-t1]);
	}
	F(i,n,pos+1) {
		int t1=dis3(p[i]),t2=dis4(p[i]),Rmx=min(T,t1-1);
		f(j,0,Rmx) f4[i][j]=f4[i+1][j]+t2;
		f(j,t1,T) f4[i][j]=min(f4[i+1][j]+t2,f4[i+1][j-t1]);
	}
}

inline bool check(int t1,int pos1,int pos2) {
	int t2=f1[pos1][t1],t3=f2[pos1+1][T-t1];
	if (t2>T||t3>T) return 0;
	int t4=f3[pos2][T-t2]+f4[pos2+1][T-t3];
	return t4<=T;
}

inline void solve(int pos) {
	if (flag_ans) return;
	sort(p+1,p+1+pos,cmp2);
	if (pos!=n) sort(p+1+pos,p+1+n,cmp2);
	get_dp(pos);
	int pos1=0,pos2=pos;
	f(t,0,T) flag_ans|=check(t,pos1,pos2);
	f(i,1,n) {
		if (pos1<pos&&dis2(p[pos1+1])==dis2(p2[i])) pos1++;
		else pos2++;
		f(t,0,T) flag_ans|=check(t,pos1,pos2);
		if (flag_ans) break;
	}
	f(i,1,n) p[i]=p1[i];
}

int main() {
	read(L);read(n);read(T);T/=2;
	f(i,1,n) read(p[i].x),read(p[i].y);
	sort(p+1,p+1+n,cmp2);
	f(i,1,n) p2[i]=p[i];
	sort(p+1,p+1+n,cmp1);
	f(i,1,n) p1[i]=p[i];
	f(i,0,n) solve(i);
	if (flag_ans) puts("Yes");
	else puts("No");
	return 0;
}

[JOIST 2025] 外郎糕 / Uiro

题意

给定长度为 \(N\) 的序列 \(A\),有 \(Q\) 次询问,每次询问给定 \(L,R\),初始时有一个数字 \(v=0\) 和一个计数器 \(c=0\),你需要依次考虑 \(A_L,A_{L+1},A_{L+2},\dots,A_{R}\),令当前考虑的数为 \(y\),你可以选择以下两种操作中的一种:

  • \(v\leftarrow v-y,c\leftarrow c+1\);
  • \(v\leftarrow v+y\),\(c\) 不变。

你需要在保证 \(v\) 始终非负的前提下,最大化最后得到 \(c\) 的值,输出这个最大值。

\(1\le N,Q\le 2\times 10^5,1\le A_i\le 100\)。

solution

感觉比 T1 难啊。

下文中选中位置 \(i\) 表示考虑 \(A_i\) 时使用了操作,\(v\leftarrow v-A_i,c\leftarrow c+1\)。

首先将每个询问的区间看作一个新的数组单独考虑,我们需要发现一些性质:

  • 对于两个位置 \(i,j\),若 \(i<j\) 且 \(A_i>A_j\),那么一定不可能选中了位置 \(i\) 而未选中位置 \(j\),否则选中位置 \(j\) 不选位置 \(i\) 一定不劣;
  • 对于一种值 \(x\),一定存在一个分界点 \(p\),使得对于 \(A_i=x\) 的位置,若 \(i< p\) 则不选择 \(i\) 否则选择 \(i\),若不满足则有 \(i<j\) 且 \(A_i=x,A_j=x\) 满足 \(i\) 被选择而 \(j\) 没被选择,这种选法一定不优于选择 \(j\) 而不选择 \(i\);
  • 对于两种值 \(x,y\),若 \(x<y\),则最后一个不被选择的值为 \(x\) 的位置一定在第一个选择的值为 \(y\) 的位置之前,否则选择 \(x\) 最后一个原来未被选择位置而不选第一个原来选择 \(y\) 的位置一定不劣。

根据这几条性质,我们可以得到一个做法的框架:首先钦定所有位置均不选择,从小到大考虑每个值 \(x\),对于 \(A_i=x\) 的位置,选择最长的后缀使得选择后可行,答案即为最终选择的位置总数,可行指的是小于 \(x\) 的位置按照原先选择,等于 \(x\) 的位置按当前后缀选择,大于 \(x\) 的位置全部不选择,根据此策略有 \(v\) 恒非负,根据调整法可以证明是正确的。

将询问离线下来,枚举 \(x\),钦定值小于 \(x\) 的位置 \(i\) 贡献为 \(-A_i\),值大于等于 \(x\) 的位置 \(i\) 贡献为 \(A_i\),令 \(S_i\) 表示前 \(i\) 个位置的贡献和,\(num_i\) 表示前 \(i\) 个位置有多少个位置值为 \(x\),\(pos_i\) 表示第 \(i\) 个值为 \(x\) 的位置,对于每个询问,维护:\(lst\) 表示第一个选中的值为 \(x\) 的位置必须在 \(lst\) 之后(满足第三条性质的限制),\(sum\) 表示未被选中的值小于 \(x\) 的位置权值和的两倍,\(ans\) 表示值小于 \(x\) 的选中的位置个数。

对于一个询问 \((l,r)\),二分分界点,令第 \(pos_{mid}\) 及其之后的值为 \(x\) 的位置都需要被选中(需满足 \(lst\le pos_{mid}\le r\)),令 \(h(p)\) 为考虑到第 \(p\) 个位置的 \(v\) 值,那么对于 \(pos_{mid}\le p\le r\),有 \(h(p)=S_p-S_{l-1}+sum-2\times x\times (num_p-mid+1)\),因为值小于 \(x\) 且未被选中的位置都在 \(lst\) 之前,而判定区间从 \(pos_{mid}\ge lst\) 开始,所以对区间内任意 \(p\),\(sum\) 中的修正已全额生效,可以直接作为常数,对于 \(l\le p<pos_{mid}\),由于此轮选中的位置在 \(pos_{mid}\) 之后,所以 \(h(p)\) 的值和上一轮得到的值相同,而由于上一轮可行,所以一定有 \(h(p)\ge 0\),那么只需要满足对于\(pos_{mid}\le p\le r\) 有 \(h(p)\ge 0\) 即可。

由于 \(h(p)\ge 0\) 等价于 \(S_p-2\times x\times num_p\ge S_{l-1}-sum-2\times x\times (mid-1)\),所以令 \(f(p)=S_p-2\times x\times num_p\),二分出 \(mid\) 后判定条件变为 \(\min\limits_{p=pos_{mid}}^{r} f(p)\ge S_{l-1}-sum-2\times x\times (mid-1)\),由于 \(mid\) 增大区间变短,左边递增,右边递减,满足单调性,所以可以二分,对 \(f\) 建立 ST 表进行查询即可做到 \(O(1)\) 判定。

二分出分界点后,令第一个合法的 \(mid\) 为 \(res\),无合法 \(mid\) 时 \(res=num_r+1\),更新询问对应的 \(lst\leftarrow \max(lst,pos_{res-1}+1)\),\(sum\leftarrow sum+2\times x\times (res-1-num_{l-1})\),\(ans\leftarrow ans+num_r-res+1\),每个询问每层时间复杂度为 \(O(\log n)\),每层预处理出各种信息以及建出 ST 表的时间复杂度为 \(O(N\log N)\)。

令值域为 \(V\),那么总时间复杂度就是 \(O((N+Q)V\log N)\),精细实现可以做到 \(O(N\log N+QV\log N)\)。

讲讲如何精细实现,由于二分时查询左端点值一定为 \(x\),建 ST 表时实际上维护两个相邻的值为 \(x\) 的位置及其之间 \(f\) 值的最小值,再对于两个相邻的值为 \(x\) 的位置之间的 \(f\) 值做前缀 \(\min\),那么查询只需要查区间包含的所有值为 \(x\) 位置及其之间 \(f\) 值的最小值,和一段不包含值为 \(x\) 的位置的前缀即可,那么建表的长度和为 \(N\),建 ST 表的总时间复杂度为 \(O(N\log N)\)。

Code
#include<cstdio>
#include<algorithm>
#include<queue>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=2e5+10;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int n,m;
int a[N];
struct ques{int l,r,lst,sum,ans;}q[N];

struct Sparse_Table {
	int Len;
	int a[N],lg[N],mx[20][N];
	
	inline void init() {f(i,2,n) lg[i]=lg[i>>1]+1;}
	inline void push_back(int x) {a[++Len]=x;mx[0][Len]=x;}
	inline void clear() {Len=0;}
	inline void build() {F(i,Len,1) f(j,1,lg[Len-i+1]) mx[j][i]=min(mx[j-1][i],mx[j-1][i+(1<<(j-1))]);}
	
	inline int query(int l,int r) {
		if (l>r) return inf;
		int k=lg[r-l+1];
		return min(mx[k][l],mx[k][r-(1<<k)+1]);
	}
}ST;

inline void solve(int x) {
	static int s[N],num[N],pos[N],f[N];
	int cnt=0;f[0]=inf;
	f(i,1,n) {
		s[i]=s[i-1]+((a[i]<x)?-a[i]:a[i]);
		num[i]=num[i-1];
		if (a[i]==x) num[i]++,pos[++cnt]=i;
		f[i]=s[i]-2*x*num[i];
		if (a[i]!=x) f[i]=min(f[i],f[i-1]);
	}
	ST.clear();
	f(i,1,cnt-1) {
		int L=pos[i],R=pos[i+1],mn=inf;
		ST.push_back(min({f[L],f[R],f[R-1]}));
	}
	ST.build();
	f(i,1,m) {
		const int l=q[i].l,r=q[i].r,add=q[i].sum;
		int L=num[q[i].lst-1]+1,R=num[r],res=-1;
		while (L<=R) {
			int mid=L+R>>1,now=s[l-1]-add-2*x*(mid-1);
			if (min(ST.query(mid,num[r]-1),f[r])>=now) R=mid-1,res=mid;
			else L=mid+1;
		}
		res=(res==-1)?num[r]+1:res;
		q[i].ans+=num[r]-res+1;
		q[i].lst=max(q[i].lst,pos[res-1]+1);
		q[i].sum+=(res-1-num[l-1])*x*2;
	}
}

int main() {
	read(n);ST.init();
	f(i,1,n) read(a[i]);
	read(m);
	f(i,1,m) read(q[i].l),read(q[i].r),q[i].lst=q[i].l;
	f(i,1,100) solve(i);
	f(i,1,m) printf("%d\n",q[i].ans);
	return 0;
}
posted @ 2026-09-21 16:23  Ff472130  阅读(11)  评论(0)    收藏  举报