ZR 2025 NOIP 二十连测 Day 5

呜呜我错了……我再也不开太大的 vector 了呜呜……/dk /dk /dk


25noip二十连测day5

链接:link
题解:题目内

时间:4h (2025.10.20 14:00~18:00)
题目数:4
难度:

A B C D
\(\color{#52C41A}绿\) \(\color{#52C41A}绿\) \(\color{#3498DB} 蓝\) \(\color{#BFBFBF} ?\)
*1600 *1800 *2400 *?

估分:[65,85] + 100 + 5 + 5 = [175,195]
得分:85 + 32 + 5 + 5 = 127
Rank:67/128


场祭

读题。

好困难!

想了 20min A 毫无头猪。

B 看起来是图论建模的样子,但是还是毫无头猪。

!然后看见了样例解释,注意到了差为 \(8\) 的倍数这一句话,于是想到同余!然后发现一条限制就可以表示成 \(w_{a_i} \equiv w_{b_i} + c_i \pmod {d_i}\),然后注意到 \(d_i = 2^k\),所以考虑给每个 \(2^k\) 开个 dsu,一条限制就转化成了图上的边。但是考虑到在模不同 \(2^k\) 意义下万一会有不同的唯一解,那就会产生错误了,所以要在 \(\le d_i\) 的每个 dsu 内都连上这条边,似乎是得到了一个必要条件,于是把必要当充分,写写写,过样例了!

看了看 CD 暴力,发现都只会打最暴力的暴力,所以就从 A 开始打了。

然后发现 \(p_i \le 11\) 似乎可以爆搜,因为乘积的增长是非常迅速的。写写写,0ms 过掉了大样例,甚至还顺便过了 \(n \le 100\) 的大样例。

打 CD 暴力,发现暴力都不会了,分别拿了 5pts 提交答案和 5pts 特殊性质走人了。


补题

B 怎么 MLE 了/jk

原来是 vector 开的过多导致的,把 dsu 里面存连通块所有点的 vector 换成一个类似于前向星的存储方式就好了……

问了 gpt,原来一个空 vector 会存三个指针也就是 24 个字节,相当于 3 个 long long,可怕。

以后都这么写吧,能不开太多的 vector 就不开。

struct __dsu
{
	int n,d,mod,fa[N],sz[N],nxt[N],ed[N]; // nxt 下一条边,ed 连通块内最后一个点的编号
	il void init(int _n,int _d){n=_n,d=_d,mod=1<<_d; rep(i,0,n+1) fa[i]=i,nxt[i]=0,sz[i]=1,ed[i]=i;}
	il int find(int x) {return x==fa[x] ? fa[x] : fa[x]=find(fa[x]);}
	il bool merge(int a,int b,int c)
	{
		int dlt=w[d][a]-c-w[d][b];
		a=find(a),b=find(b);
		if(a==b) return 0;
		sz[a]<sz[b] && (swap(a,b),dlt=-dlt);
		d<16 && (dlt=(dlt+mod*10)%mod,1);
		
		fa[b]=a,sz[a]+=sz[b],nxt[ed[a]]=b,ed[a]=ed[b];
		for(int u=b;u;u=nxt[u]) w[d][u]+=dlt,d<16 && (w[d][u]%=mod);
		
		return 1;
	}
} dsu[17];

哦好像还有更优秀的做法,只需要记一个 fa 数组然后在 find 的时候根据 fa 的标记来修改(类似于懒标记),太牛了。

补 A,有点巧妙了。还是注意到乘积不会增长地很快,但是这有什么用呢?

因为每个数都必须选入一个组内,所以总和是固定的,又因为没有 \(<2\) 的数,所以组成乘积的数之和一定很小,于是组成和的数之和一定很大!进而乘积也一定是非常接近 \(sum = \sum a_i p_i\) 的!所以可以考虑枚举这个来做。

然后就容易了,考虑枚举一个乘积 \(x = sum - \varepsilon\),又因为 \(p_i\) 都是质数,所以直接对 \(x\) 分解质因数就能判断是否能有乘积正好 \(=x\)。然后又因为 \(x\) 的唯一分解是唯一的,所以在分解质因数的过程中就可以顺便把组成和的数之和算出来了,判断是否相等即可。

注意此处分解质因数并不需要 \(O(\sqrt x)\) 分解,只需要对于所有 \(p_i\) 除一遍即可,最后如果剩下的数 \(>1\),那么一定不合法。

补 C,哦一部分要用到哈夫曼树,几年前学过来着但是早忘了 /ll

不怎么难的,想到哈夫曼树的计算方式(按位拆贡献)就比较容易了。

首先 \(A=B\) 就是直接建哈夫曼树,所以只考虑 \(A=1,B=2\) 的情况(反过来也没区别)。

考虑最后构成答案的所有串一定构成的一棵 01trie,且只有叶子节点会和 \(a_i\) 匹配来产生贡献,而且显然深度(从根节点到叶子节点的带权路径,每个 \(0\) 边贡献为 \(1\),每个 \(1\) 边贡献为 \(2\))越低的点和越大的 \(a_i\) 匹配。

因为 \(n\) 很小,所以考虑 dp,发现我们需要记录当前深度、在当前深度的节点个数、已经匹配的 \(a_i\) 个数,以及在下一深度的节点个数,因为无法避免地,在由一个深度为 \(x\) 的节点往下推的时候会产生深度分别为 \(x+1,x+2\) 的两个节点。

转移考虑是在当前深度选一个节点和 \(a_i\) 匹配,还是把当前深度剩下的所有点都推到下一深度即可,显然并不需要单独考虑只推一个节点的情况。

但是这样是 \(O(n^4)\) 的呀!

看看哪一维可以删掉,发现其实并不需要真的去记录深度!而是可以类似哈夫曼树地,把贡献拆到每个节点上。那么对于每个同一深度的节点,如果这个深度不再匹配新的 \(a_i\),那么这些点加起来一定会产生剩下的 \(a_i\) 之和的贡献,也就一定是 \(a_i\) 的一段后缀和,此处我们认为 \(a_i\) 已经从大到小排好序了。

于是就是 \(O(n^3)\) 的了。

具体地,令 \(f_{i,j,k}\) 为已经匹配了 \(i\)\(a_x\),当前深度剩下 \(j\) 个点,下一深度有 \(k\) 个点的最小代价,令当前深度为 \(x\),有转移:

  • 直接推到下一深度,那么深度为 \(x+1,x+2\) 的点分别有 \(j+k\) 个和 \(j\) 个(\(k\) 是原有的,\(j\) 是推出来的)。

    \[f_{i,j,k} + \sum _{p=i+1} ^n a_p \to f_{i,j+k,j} \]

  • 匹配一个 \(a_i\)

    \[f_{i,j,k} \to f_{i+1,j-1,k} \]


天依宝宝可爱!

posted @ 2025-10-20 20:42  little__bug  阅读(24)  评论(0)    收藏  举报