11.6 校内NOIP模拟赛总结 + 题解(折半搜索 + 并查集 + dp)

时间安排

感觉啥也没干的一场。

  • \(7:40 - 8:00\) 开题。这题目名有点nb。\(T1\) 是糖题。 \(T2\) 数据规模有点幽默,或许是搜索? \(T3\) 看起来是什么套路题。 \(T4\) 没咋看。
  • \(8:00 - 8:20\)\(T1\),应该是直接贪心然后用一个最大子段和check就行。最开始写的 \(dp\) 形式,没过小样例。调了一会儿测试大样例发现又没过。感觉有点难泵。然后发现写成区间相减的形式会少很多细节。改了改过大样例了就跳了。
  • \(8:20 - 8:40\)\(T2\)\(n, m \leq 15\) 感觉挺幽默,但是一想发现只会 \(2^{n + m}\) 的搜索。然后如果我枚举一维那么另一维就是一个只有 \(15\) 个物品,但是体积很大的背包。这我咋 \(dp\) ???然后就开始想乱搞。然后想到一个看起来还挺对的做法:第二维直接随机化。计算了一下,随机到正解的概率为三百多分之一,概率比较低,想要随机到正确答案需要的次数比较多,可能会超时。然后就去看 \(T3\) 了。
  • \(8:40 - 9:00\)\(T3\),比较快的想出来了一个双 \(log\) 的做法,可以得 \(60pts\),但是非常难写。然后就是在纠结开始写 \(T3\) 还是先写 \(T2\)
  • \(9:00 - 9:10\) 上个厕所冷静一下。路上意识到 \(T2\) 可以对第二维折半搜索,感觉复杂度比较小应该可以直接过,而且不难写。于是回去后开始写 \(T2\)
  • \(9:10 - 9:35\) 写完 \(T2\),发现可以直接过小样例,然后测试大样例也过了。并且最大的数据跑了 \(0.5s\) ,感觉没什么问题,并且此时已经过了挺长时间了,就没有测试先过了。
  • \(9:35 - 9:50\) 想出了 \(T3\) 的单 \(log\) 做法。只需要把启发式合并换成线段树合并即可。很开心,然后开写。
  • \(9:50 - 10:40\)\(log\) 做法非常难写,写道一半发现会 \(MLE\),然后有点急,就先改成了双 \(log\)。启发式合并比较好写,一会儿就写完了,但是也写了 \(200\) 行。然后测试样例发现 \(RE\) 了,就开始调试,调出来几个 \(bug\) 后能跑过 \(1e5\) 的数据了。尝试把数组开大,发现最大的数据也能正确跑出来,但是需要 \(5s\)
  • \(10:40 - 11:00\) 加上了 \(IO\) 读入,最大的数据能跑到 \(2s\),然后还纠结了一会儿要不要改一改,写成单 \(log\)。但是时间已经完全不够了, \(T4\) 连暴力都没有。就放弃去看 \(T4\) 了。
  • \(11:00 - 12:00\)\(T4\),很快会了 \(50pts\)。然后就先把 \(50pts\),测试了一下,发现没有问题。尝试另外 \(20pts\),但是没有什么头绪。比赛最后 \(20min\) 没有得分。

成绩

期望得分:100 + 100 + 60 + 50 = 310
实际得分:100 + 71 + 60 + 45 = 278
rk2 被 \(forgive\) 吊打
有点挂分。

总结

  • \(T2\) 会的太慢了,导致后面的题没什么时间。以后不能把问题想得太复杂。
  • \(T3\) 想复杂了,实际上中间的转化都是对的,就是最后一步可以直接并查集而我写的模拟路径覆盖,这样不但复杂度上去了,而且代码量还很大。
  • \(T4\) 70 分实际上就是贝尔数复杂度。这个复杂度写的题还是少,考试时想不起来。

题解

T2 (折半搜索)

题意:
给你一个 \(n \times m\) 的矩阵,每个位置上有一个数字 \(a_{i, j}\)。你可以选择删掉若干行和若干列,使得剩下没有被删的数字之和为 \(S\)。如果存在一种方案,输出一组合法解。

\(1 \leq n, m \leq 15\)\(0 \leq a_{i, j} \leq 10^9\)\(1 \leq S \leq 10^8\)

分析:
简单题。注意到 \(n, m\) 范围很小,但是同时枚举会 \(TLE\)。考虑二进制枚举每一行是否删除,然后每列删除的价值就定下来了。你需要选择若干列使得和为定值,这是经典的背包问题,直接折半搜索就行。复杂度 \(O(2^{n + \frac{m}{2}} \times m)\)

考试时 \(TLE\) 的原因是 每次需要把 \(unordered \ map\) \(.clear()\),否则 \(map\) 里面的元素会越来越多导致 \(TLE\) 或者 \(MLE\)

T3(并查集)

题意:
给你一张无向图 \(n\) 个点, \(m\) 条边。
\(q\) 次操作,每次有两种操作:
1, u 删掉编号为 \(u\) 的边,保证这条边之前没有被删除。
2 u, v 询问是否存在两条从 \(u\)\(v\) 且边不相交的路径,如果可以输出 YES,否则输出 NO

\(1 \leq n \leq 800000\)\(1 \leq m, q \leq 1000000\)。保证不存在自环,图不保证连通。

分析:
首先每次询问就是问你从 \(u, v\) 之间是否存在割边。
在一棵生成树上考虑这个问题。那么对于非树边,它们一定不是割边。对于树边而言:如果有非树边覆盖它,那么它也不是割边,否则就是割边。
那么有一个简单的想法就是每次删除一条非树边就把它覆盖的树边减 \(1\),然后查询就变成了查询路径上最小值是不是小于等于 \(0\)
发现如果非树边比树边先删除,那么树的形态会改变,上面的过程就是不对的。可以将每条边按照最后被删除的时刻从大到小排序,然后以这个顺序建出一棵生成树,这棵生成树满足所有非树边的删除时刻都早于它覆盖的树边。
那么维护上述过程的做法就很多了:可以直接树剖,也可以先求出来每条树边最后没有非树边覆盖的最早时刻,然后扫描线。第二种做法可以用 线段树合并 做到单 \(log\)

正解是线性的:
考虑倒着处理操作,那么每次从删边变成了加边。我们维护最小生成树的想法不改变。那么每次操作如果加入了一条树边,就不用管。如果加入了一条非树边,那么这条非树边覆盖的所有树边已经加入过了,需要对他们打上被覆盖过的标记。
由于一条树边被一条非树边覆盖就足够了,那么我们可以在加入非树边的时候把 \(u, v\)\(lca\) 上的点合并成一个点,表示 这些点之间没有割边。那么查询只需要判断两个点是否在同一个联通块里。
由于一条树边只会被合并一次,那么每次合并就可以暴力往上跳。复杂度是线性的。

CODE:

#include<bits/stdc++.h>
#define pb emplace_back
using namespace std;
const int N = 1e6 + 10;
struct IO{
    static const int S=1<<21;
    char buf[S],*p1,*p2;int st[105],Top;
    ~IO(){clear();}
    inline void clear(){fwrite(buf,1,Top,stdout);Top=0;}
    inline void pc(const char c){Top==S&&(clear(),0);buf[Top++]=c;}
    inline char gc(){return p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++;}
    inline IO&operator >> (char&x){while(x=gc(),x==' '||x=='\n'||x=='\r');return *this;}
    template<typename T>inline IO&operator >> (T&x){
        x=0;bool f=0;char ch=gc();
       while(!isdigit(ch)){if(ch=='-') f^=1;ch=gc();}
        while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=gc();
        f?x=-x:0;return *this;
    }
    inline IO&operator << (const char c){pc(c);return *this;}
    template<typename T>inline IO&operator << (T x){
        if(x<0) pc('-'),x=-x;
        do{st[++st[0]]=x%10,x/=10;}while(x);
        while(st[0]) pc('0'+st[st[0]--]);return *this;
    }
}fin, fout;
int n, m, q, id;
int bin[N];
int u[N], v[N], idx[N];
int tim[N];
int opt[N], uu[N], vv[N];
int dep[N], fat[N];
bool Te[N];
bool ans[N];
int head[N], tot;
struct edge {
	int v, last;
}E[N * 2];
inline void add(int x, int y) {
	E[++ tot].v = y;
	E[tot].last = head[x];
	head[x] = tot;
}
inline bool cmp(int x, int y) {
	return tim[x] > tim[y];
}
void dfs(int x, int fa) {
	fat[x] = fa; dep[x] = dep[fa] + 1;
	for(int i = head[x]; i; i = E[i].last) {
		int v = E[i].v;
		if(v == fa) continue;
		dfs(v, x);
	}
}
int Find(int x) {return x == bin[x] ? x : bin[x] = Find(bin[x]);}
inline void Bin(int x, int y) {
	int f1 = Find(x), f2 = Find(y);
	if(f1 != f2) bin[f1] = f2;
}
inline void Merge(int u, int v) { // 把 u, v 合并起来 
	int nu = Find(u), nv = Find(v);
	while(1) {
		if(nu == nv) break;
		if(dep[nu] >= dep[nv]) {
			Bin(nu, fat[nu]);
			nu = Find(nu);
		}
		else {
			Bin(nv, fat[nv]);
			nv = Find(nv);
		}
	}
}
int main() {
	fin >> n >> m >> q >> id;
	for(int i = 1; i <= n; i ++ ) bin[i] = i;
	for(int i = 1; i <= m; i ++ ) {
		fin >> u[i] >> v[i];
		tim[i] = q;
		idx[i] = i;
	}
	for(int i = 1; i <= q; i ++ ) {
		fin >> opt[i];
		if(opt[i] == 1) {fin >> uu[i]; tim[uu[i]] = i;}
		else fin >> uu[i] >> vv[i];
	}
	sort(idx + 1, idx + m + 1, cmp);
	for(int i = 1; i <= m; i ++ ) {
		int o = idx[i];
		int f1 = Find(u[o]), f2 = Find(v[o]);
		if(f1 != f2) {
			add(u[o], v[o]); 
		    add(v[o], u[o]);
			bin[f1] = f2;
			Te[o] = 1;
		}
	}
	for(int i = 1; i <= n; i ++ ) {
		if(!fat[i]) dfs(i, 0);
	}
	for(int i = 1; i <= n; i ++ ) bin[i] = i;
	int j = 1;
	for(int i = q; i >= 1; i -- ) {
		while(j <= m && tim[idx[j]] >= i) {
			if(Te[idx[j]]) {j ++; continue;}
			else {
				Merge(u[idx[j]], v[idx[j]]);
				j ++;
			}
		}
		if(opt[i] == 2) {
			int U = uu[i], V = vv[i];
			if(Find(U) == Find(V)) ans[i] = 1;
			else ans[i] = 0;
		}
	}
	for(int i = 1; i <= q; i ++ ) {
		if(opt[i] == 2) {
			if(ans[i]) fout.pc('Y'), fout.pc('E'), fout.pc('S'), fout.pc('\n');
			else fout.pc('N'), fout.pc('O'), fout.pc('\n');
		}
	}
	return 0;
}

T4(搜索,树形dp,贝尔数)

题意:
给你一张 \(n\) 个点, \(m\) 条边的无向图,你需要给图上所有点和边都染上颜色。
首先需要对每个节点染色,可以染成 \(1 \sim k\) 中的一种颜色。
接着对每条边染色。对于第 \(i\) 条边,假设它连接了 \(u_i, v_i\),那么:

  • 如果 \(u_i, v_i\) 颜色不同,这条边可以染 \(A_i\) 种颜色。
  • 如果 \(u_i, v_i\) 颜色相同,这条边可以染 \(B_i\) 种颜色。

\(A_i\)\(B_i\) 包含的颜色都是不同的。也就是一条边总共可能染出的颜色有 \(A_i + B_i\) 种。
问总共有多少种不同的染色方案。两种方案不同当且仅当吗某个节点或某条边颜色不同。
答案对 \(10^9 + 7\) 取模。

\(0 \leq A_i, B_i \leq 10^9\)

3

分析:
如果图的形态是一棵树,那么做法是简单的。直接设 \(f_{i}\) 表示考虑了以 \(i\) 为根的子树,\(i\) 的颜色还没有染的方案数。
转移有两种,对于 \(x\) 的儿子 \(v\)

  • \(x\)\(v\) 颜色不同。 \(f_{x} \gets f_{x} \times f_v \times (k - 1) \times A_i\)
  • \(x\)\(v\) 颜色相同。 \(f_{x} \gets f_{x} \times f_{v} \times B_{i}\)

最后答案就是 \(f_{1} \times k\)

然后前面的一些部分分就是搜出每个点的颜色,但是我们只关心哪些点颜色相同,所以这个可以做到 \(O(bell(n) \times m)\)

对于 \(n = 89, m = 98\) 的数据:发现非树边只有 \(9\) 条,那么我们要算非树边的贡献,有一个比较简单的想法是我们直接搜索所有非树边连接的 \(18\) 个点的颜色,这一部分复杂度是 \(O(bell(18))\) 的。然后就可以不考虑这些非树边,做一遍树形 \(dp\)

\(dp\) 状态可以设 \(dp_{x, c}\) 表示考虑 \(x\) 的子树, \(x\) 的颜色为 \(c\) 的方案数。 \(c \in [0, cnt]\)\(cnt\) 为搜出来的集合数, \(c = 0\) 代表不是 \(cnt\) 种任意一种颜色。然后初始值为:对于非特殊点, \(dp_{x, i} = 1\)。对于特殊点, \(dp_{x, c_i} = 1\),其余为 \(0\)。转移可以优化到 \(O(c)\),总复杂度就是 \(O(T \times bell(18) \times n \times cnt)\),不可能通过。

但是我们发现我们在 \(dp\) 过程种同样考虑每个点的颜色,那么对于一条非树边连接两个点 \(u, v\)。实际上我们可以只确定其中一个点的颜色就足够了。假如我们确定了 \(u\) 的颜色为 \(c\),那么当我们在求 \(dp_{v, cc}\) 的时候,将这条非树边的贡献乘进去就行了。这样搜索的复杂度就变成了 \(O(bell(9))\) 了,总复杂度为 \(O(T \times bell(9) \times n \times cnt)\)\(bell(9) = 21147\)\(cnt\) 最大取到 \(9\),但是肯定跑不满,是可以过的。

实际上在写的过程中非常容易 \(TLE\),调了好长时间发现每次关键点的个数都能够卡到上界。可以枚举一条非树边到底使用哪一个点作为关键点,让关键点尽量重复,这样搜索的复杂度就可以降下来。(但是实际上还是可以卡满的,不知道为啥这样就能过)

CODE:

#include<bits/stdc++.h>
#define pb emplace_back
using namespace std;
typedef long long LL;
const int mod = 1e9 + 7;
const int N = 120;
const int M = 20;
int T, n, m, tot, head[N];
struct edge {
	int v, last, idx;
}E[N * 2];
int a[N], b[N], K; // 不同为 a, 相同为 b 
int dp[N][M], res; // dp[i][j] 表示i为根的子树, i的颜色为j的方案数。j = 0 代表一种与钦定的颜色不一样的颜色 
bool Te[N]; // 是否为树边 
int bin[N], col[N]; // 每个关键点的颜色 
int u[N], v[N];
bool key[N];
int node[N], Len; // 关键点 
int Find(int x) {return x == bin[x] ? x : bin[x] = Find(bin[x]);}
int tims = 0;
void add(int u, int v, int idx) {
	E[++ tot].v = v;
	E[tot].idx = idx;
	E[tot].last = head[u];
	head[u] = tot;
}
int cnt; // cnt 代表集合数 
int vec[N][N];
int len[N];
inline int A(LL n, LL m) {
	int res = 1LL;
	for(int i = n; i >= n - m + 1; i -- ) res = 1LL * res * i % mod;
	return res;
}
void DP(int x, int fa) {
	if(!key[x]) for(int j = 0; j <= cnt; j ++ ) dp[x][j] = 1LL; // 1代表其余的一种颜色,最后算贡献 
	else for(int j = 0; j <= cnt; j ++ ) dp[x][j] = (j == col[x]);
	for(int i = head[x]; i; i = E[i].last) {
		int v = E[i].v, idx = E[i].idx;
		if(v == fa && Te[idx]) continue;
		else if(Te[idx]) { // 树边 
			DP(v, x);
            LL S = 0;
            for(int j = 1; j <= cnt; j ++ ) S = S + dp[v][j];
            S = S % mod;
            int SS = (int)(S);
            for(int j = 0; j <= cnt; j ++ ) {
            	if(j == 0) {
            		int val = 0;
					val = 1LL * S * a[idx] % mod;
					val = (val + 1LL * dp[v][0] * (1LL * (K > cnt ? K - cnt - 1 : 0) * a[idx] % mod + b[idx])) % mod;
					dp[x][j] = 1LL * dp[x][j] * val % mod;
				}
				else {
					int val = 0;
					val = (1LL * val + 1LL * dp[v][0] * (K - cnt) % mod * a[idx]) % mod;
					val = (1LL * val + (1LL * S - dp[v][j] + mod) * a[idx]) % mod;
					val = (1LL * val + 1LL * dp[v][j] * b[idx]) % mod;
					dp[x][j] = 1LL * dp[x][j] * val % mod;
				}
			}
		}
		else if(u[idx] != x){ // 非树边 
			for(int j = 0; j <= cnt; j ++ ) {
				if(j == col[v]) dp[x][j] = (1LL * dp[x][j] * b[idx]) % mod;
				else dp[x][j] = (1LL * dp[x][j] * a[idx]) % mod;
			}
		}
	}
}
inline LL calc() { // 现在已经钦定了cnt种颜色 
	if(cnt > K) return 0;
	int res = A(K, cnt); 
	DP(1, 0); // 树形dp 
	int S = 0;
	for(int i = 0; i <= cnt; i ++ ) {
		S = (1LL * S + 1LL * dp[1][i] * (i == 0 ? (K - cnt) : 1LL)) % mod;
	}
	return 1LL * res * S % mod;
}
void dfs(int x) {
	if(x == Len + 1) {
		res = (res + calc()) % mod;
		return ;
	}
	for(int i = 1; i <= cnt; i ++ ) {
		vec[i][++ len[i]] = node[x];
		col[node[x]] = i;
		dfs(x + 1);
		len[i] --;
	} 
	cnt ++;
	vec[cnt][++ len[cnt]] = node[x];
	col[node[x]] = cnt;
	dfs(x + 1);
	len[cnt] --;
	cnt --;
}
int seq[N];
void solve() {
	res = 0;
	tot = 0;Len = 0; 
	memset(head, 0, sizeof head); 
	memset(key, 0, sizeof key);
	scanf("%d%d%lld", &n, &m, &K);
	for(int i = 1; i <= n; i ++ ) bin[i] = i;
	int len = 0;
	for(int i = 1; i <= m; i ++ ) {
		scanf("%d%d%d%d", &u[i], &v[i], &a[i], &b[i]);
		if(u[i] > v[i]) swap(u[i], v[i]);
		int f1 = Find(u[i]), f2 = Find(v[i]);
		if(f1 != f2) {
			Te[i] = 1;
			bin[f1] = f2;
		}
		else {
			Te[i] = 0;
			seq[++ len] = i;
			node[++ Len] = u[i]; 
		}
		add(u[i], v[i], i);
		add(v[i], u[i], i);
	}
	int mx = 1e9, s = 0;
	if(m - n <= 15) {
		for(int S = 0; S < (1 << len); S ++ ) {
			vector< int > vec;
			for(int j = 0; j < len; j ++ ) {
				if((S >> j) & 1) vec.pb(u[seq[j + 1]]);
				else vec.pb(v[seq[j + 1]]);
			}
			sort(vec.begin(), vec.end());
			int cnt = 0;
			for(int j = 1; j < vec.size(); j ++ ) cnt += (vec[j] != vec[j - 1]);
			if(cnt < mx) {mx = cnt; s = S;}
		}
		for(int j = 0; j < len; j ++ ) {
			if((s >> j) & 1) node[j + 1] = u[seq[j + 1]];
			else node[j + 1] = v[seq[j + 1]], swap(u[seq[j + 1]], v[seq[j +  1]]);
		}
	}
	sort(node + 1, node + Len + 1);
	Len = unique(node + 1, node + Len + 1) - (node + 1);
	for(int i = 1; i <= Len; i ++ ) key[node[i]] = 1;
	dfs(1); // 对关键点爆搜, 复杂度bell(len) 
	printf("%d\n", res);
}
int main() {
	scanf("%d", &T);
	while(T -- ) {
		solve();
	}
	return 0;
}
posted @ 2026-09-12 10:14  Czl2007  阅读(5)  评论(0)    收藏  举报