[省选前集训2021] 模拟赛4

总结

打暴力是真的香,竟然用暴力超过了 \(\tt OneInDark\)考试就应该跪着打部分分

第二题本来摸到正解的门槛了,但是脑袋剧烈的疼痛,想题的时候还是要追求严谨一点,要不然就很容易走弯路了,但是写的时候不要怕用乱贪心骗分(今天第二题就骗了 \(20\) 分)

No Time to Dry P

题目描述

点此看题

解法

这种会覆盖的涂色问题就考虑从小到大插入颜色段的方式就可以了,想一想就知道答案是两个相邻相同颜色之间有没有比它小的颜色这个判断式之和,然后我直接写了一个莫队,时间复杂度 \(O(n\sqrt n)\)

其实可以二维数点,时间复杂度 \(O(n\log n)\)

#include <cstdio>
#include <algorithm>
using namespace std;
const int M = 200005;
const int bk = 600;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,sum,l,r,a[M],pre[M],suf[M];
int ans[M],ls[M],lg[M],dp[M][20];
struct node
{
	int l,r,id;
	bool operator < (const node &b)
	{
		if(l/bk!=b.l/bk) return l<b.l;
		return r<b.r;
	}
}q[M];
void init()
{
	for(int i=1;i<=n;i++)//预处理前驱 
	{
		pre[i]=ls[a[i]];
		ls[a[i]]=i;
	}
	for(int i=1;i<=n;i++) ls[i]=0;
	for(int i=n;i>=1;i--)//预处理后继
	{
		suf[i]=ls[a[i]];
		ls[a[i]]=i;
	}
	for(int j=1;(1<<j)<=n;j++)//预处理st表 
		for(int i=1;i+(1<<j)-1<=n;i++)
			dp[i][j]=min(dp[i][j-1],dp[i+(1<<j-1)][j-1]);
}
int get(int l,int r)//求两点间的最小值 
{
	int k=lg[r-l+1];
	return min(dp[l][k],dp[r-(1<<k)+1][k]); 
}
void upl(int x,int f)
{
	if(l<=suf[x] && suf[x]<=r)
	{
		int tmp=get(x,suf[x]);//最小编号 
		if(tmp<a[x]) sum+=f;//有贡献 
	}
	else sum+=f;//否则是第一次出现 
}
void upr(int x,int f)
{
	if(l<=pre[x] && pre[x]<=r)
	{
		int tmp=get(pre[x],x);
		if(tmp<a[x]) sum+=f;
	}
	else sum+=f;
}
int main()
{
	freopen("notime.in","r",stdin);
	freopen("notime.out","w",stdout);
	n=read();m=read();
	for(int i=1;i<=n;i++)
	{
		a[i]=dp[i][0]=read();
		if(i>1) lg[i]=lg[i>>1]+1;
	}
	init();
	for(int i=1;i<=m;i++)
	{
		int l=read(),r=read();
		q[i]=node{l,r,i};
	}
	sort(q+1,q+1+m);
	l=1,r=0;
	for(int i=1;i<=m;i++)
	{
		int L=q[i].l,R=q[i].r;
		while(L<l) upl(--l,1);
		while(L>l) upl(l++,-1);
		while(R>r) upr(++r,1);
		while(R<r) upr(r--,-1);
		ans[q[i].id]=sum;
	}
	for(int i=1;i<=m;i++)
		printf("%d\n",ans[i]);
}

Minimizing Edges P

题目描述

点此看题

这道题基本不涉及任何算法,但是思维难度极高。

网上没有这道题的题解,我的思路是从官方题解那里学来的,希望能帮到你。

解法

首先讲一讲为什么我考试的时候会暴毙掉,因为我觉得最优解的构造是和原图紧密相关的,所以我在原树上搞,然后就凉了。因为这道题的提问方式是构造一个新图,没有人告诉你一定要在原图上面改,所以在适当的时候一定要抛弃原图


首先可以求出数组 \(a[i][0/1]\) 表示到 \(i\) 的奇数最短路和偶数最短路,不难发现因为可以反复经过最近的一条边,所以两个图相等当且当且仅当跑出来的 \(a\) 数组相等,\(a\) 数组可以很容易 \(O(n)\) 直接 \(\tt bfs\) 求出。

求出原图的 \(a\) 数组之后就不要管原图了,设 \(h(i)=(\min(a[i][0/1]),\max(a[i][0/1]))\) 就表示按大小排的奇偶路径二元组,首先如果原图是二分图那么特判掉,答案是 \(n-1\)(因为二分图的情况 \(a[i][0/1]\) 有一个没有意义,所以特判),然后我们来考虑点 \(i\) 合法的时候满足什么条件,点 \(1\) 太特殊了,所以我们暂时不考虑它,设 \(h(i)=(x,y)\)

  • 如果和 \(j\) 连边就直接让他满足条件了,那么 \(h(j)=(x-1,y-1)\)
  • 如果 \(x+1<y\),同时和 \(j_1,j_2\) 连边(一个点解决 \(x\) 或者解决 \(y\)),则 \(h(j_1)=(x-1,y+1),h(j_2)=(x+1,y-1)\)
  • 如果 \(x+1=y\),同时和 \(j_1,j_2\) 连边,则 \(h(j_1)=(x-1,y+1),h(j_2)=(x,y)\)

再来多解释下上面的结论,首先为什么情况这么唯一呢?因为 \(\tt tly\) 巨佬告诉我们了一个三角形不等式(?),也就是有边相连的点的最短路只会绝对值之差小于等于 \(1\),那么就只用讨论 \(x-1,x,x+1\) 这些情况,然后为了保证第一维小于等于第二维所以才讨论一下分出第二种情况和第三种情况。

现在考虑怎么用这个结论来做题吧,我们把 \((x,y)\) 相同的点分到一起,然后你发现情况 \(2/3\)\(x+y\) 和一定,所以可以把 \(x+y\) 一定的点划分成一层,同一层的按 \(x\) 排序,这样 \(h(j)\) 就是连向以前层的,\(h(j_1)\) 是连向同层但是是之前的点,还是分情况来讨论,设 \(S(x,y)\) 表示 \(h(i)=(x,y)\) 这类点的点数,那么有这样几种情况:

下面就直接用贪心做了,因为第一种情况只用连一条边,而二三种情况可能会连两条边,依据这个为主要思想贪心即可。

  • \(S(x-1,y-1)\not=0\and S(x-1,y+1)=0\),那么直接无脑连向 \((x-1,y-1)\) 即可。
  • \(S(x-1,y-1)=0\and S(x-1,y+1)\not=0\),那么拿到以前 \((x-1,y+1)\) 连向 \((x,y)\) 的边数 \(cnt\),然后增加 \(\max(0,S(x,y)-cnt)\) 即可,再根据 \(x,y\) 的关系可以知道是连向 \((x,y)\) 还是 \((x+1,y-1)\),如果是连向 \((x+1,y-1)\) 那么就修改一下它的 \(cnt\) 即可。
  • \(S(x-1,y-1)\not=0\and S(x-1,y+1)\not=0\),那么以前有的 \(cnt\) 条边只需要再增加一条边就可以使得这个点合法,而且会让 \((x+1,y-1)\)\(cnt\) 增加,所以我们优先执行这个操作。如果这 \(cnt\) 条边用完了那么我们连向 \((x-1,y-1)\) 即可。

具体实现就写一个 \(\tt map\) 即可,还有就是上面都没有考虑 \(1\) 这个特殊的点,如果 \(1\) 有自环的话那么需要特判,直接输出 \(n\) 即可。然后就耐心地讨论吧,时间复杂度 \(O(n\log n)\)

#include <cstdio>
#include <vector>
#include <iostream>
#include <algorithm>
#include <queue>
#include <map>
using namespace std;
#define pii pair<int,int>
#define make make_pair 
const int M = 200005;
const int inf = 1e9;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int T,n,m,k,ans,tot,f[M],a[M][2];
map<pii,int> mp,cnt;
struct edge
{
	int v,next;
	edge(int V=0,int N=0) : v(V) , next(N) {}
}e[2*M];
struct node
{
	int x,y;
	bool operator < (const node &b) const
	{
		if(x+y!=b.x+b.y) return x+y<b.x+b.y;
		return x<b.x;
	}
}b[M];
void bfs()
{
	for(int i=1;i<=n;i++)
		for(int j=0;j<=1;j++)
			a[i][j]=inf;
	queue<int> q;
	q.push(1);a[1][0]=0;
	while(!q.empty())
	{
		int u=q.front();q.pop();
		for(int i=f[u];i;i=e[i].next)
		{
			int v=e[i].v;
			if(a[u][0]<inf && a[v][1]==inf)
			{
				a[v][1]=a[u][0]+1;
				q.push(v);
			}
			if(a[u][1]<inf && a[v][0]==inf)
			{
				a[v][0]=a[u][1]+1;
				q.push(v);
			}
		}
	}
}
void solve()
{
	bfs();
	if(a[1][1]==1)//1有自环
	{
		printf("%d\n",n);
		return ;
	}
	for(int i=1;i<=n;i++)//是二分图 
		if(a[i][0]==inf || a[i][1]==inf)
		{
			printf("%d\n",n-1);
			return ;
		}
	for(int i=1;i<=n;i++)//把每个点分类 
	{
		pii t=make(a[i][0],a[i][1]);
		if(t.first>t.second) swap(t.first,t.second);
		mp[t]++;
		if(mp[t]==1)//加到数组上面排序
			b[++k]=node{t.first,t.second};
	}
	sort(b+1,b+1+k);
	for(int i=1;i<=k;i++)
	{
		int x=b[i].x,y=b[i].y;
		pii t=make(x,y),t3=make(x+1,y-1);
		pii t1=make(x-1,y-1),t2=make(x-1,y+1);
		if(mp[t1]>0 && mp[t2]==0)
			ans+=mp[t];//直接连上去
		if(mp[t1]==0 && mp[t2]>0)
		{
			ans+=max(0,mp[t]-cnt[t]);//补连向j1的边
			if(x+1==y)//是第三种情况
				ans+=(mp[t]+1)/2;//上取整 
			else
			{
				ans+=mp[t];//连到(x+1,y-1)去
				cnt[t3]+=mp[t]; 
			}
		}
		if(mp[t1]>0 && mp[t2]>0)
		{
			if(mp[t]>cnt[t])
				ans+=mp[t]-cnt[t];//连(x-1,y-1)
			cnt[t]=min(cnt[t],mp[t]);//别忘了写这个 
			if(x+1==y)
				ans+=(cnt[t]+1)/2;
			else
			{
				ans+=cnt[t];
				cnt[t3]+=cnt[t];
			}
		}
	}
	printf("%d\n",ans);
}
int main()
{
	//freopen("minimizing.in","r",stdin);
	//freopen("minimizing.out","w",stdout);
	T=read();
	while(T--)
	{
		mp.clear();
		cnt.clear();
		n=read();m=read();
		tot=1;k=0;ans=0;
		for(int i=1;i<=n;i++)
			f[i]=0;
		for(int i=1;i<=m;i++)
		{
			int u=read(),v=read();
			e[++tot]=edge(v,f[u]),f[u]=tot;
			e[++tot]=edge(u,f[v]),f[v]=tot;
		}
		solve();
	}
}

Counting Graphs P

题目描述

点此看题

一道需要肝的毒瘤 \(dp\) 题。

解法

二分图的时候特判,否则像上面那道题做,先把层分出来。

\(dp(x,y,num)\) 表示之前层以及同层的在 \(S(x,y)\) 之前的点集都已经合法,同时 \(S(x,y)\) 只向 \(S(x-1,y-1),S(x-1,y+1),S(x,y)\) 连了边(也就是只考虑之前的边),此时还有恰好 \(num\) 个点还没有合法的图的数量(这 \(num\) 个点就给后面考虑,这很 \(dp\)),先约定几个辅助值:

无重边有标号二分图左边的点数是 \(p\),右边的点数是 \(q\),设 \(F(p,q)=2^{pq}\) 表示无限制图的总数。

\(G(p,q)\) 表示每个点都至少连出去了一条边的方案数,转移考虑左半部分的最后一个点怎么选择,枚举右边已经有边连出去的点,剩下的点就需要这个这个左半部分的点连向它们:

\[G(p,q)=\sum_{0\leq i\leq q}G(p-1,i){q\choose i}(2^i-[i=q]) \]

\(H(p,q)=(2^p-1)^q\) 表示右边部分的每个点都至少连了一条边的图种类数。

无重边有标号图\(p\) 个点,设 \(P(p)=2^{\frac{p(p+1)}{2}}\),表示无限制图的种类数。

\(Q(p)\) 表示每个点至少都连了一条边的图种类数,计算这个直接容斥没有边的点数就好:

\[Q(p)=\sum_{0\leq i\leq p}(-1)^i{p\choose i}P(p-i) \]


下面直接开始我们的大讨论,按照上一题那个框架一个一个来就可以了。

Case1:|S(x-1,y+1)|=0

如果 \(x+1<y\),那么只能向 \(S(x-1,y-1)\) 连边,用上一层最后那一个就可以了:

\[dp(x,y,0)=dp(\frac{x+y}{2}-1,\frac{x+y}{2},0)H(|S(x-1,y-1)|,|S(x,y)|) \]

如果 \(x+1=y\),那么可以向自己连边,只不过是随意选择的:

\[dp(x,y,0)=dp(\frac{x+y}{2}-1,\frac{x+y}{2},0)H(|S(x-1,y-1)|,|S(x,y)|)P(|S(x,y)|) \]

Case2:|S(x-1,y+1)|>0

那么现在要考虑 \(dp(x-1,y+1,n_1)\)\(dp(x,y,n_2)\) 的转移了。

还有一点要提醒的是 \(S(x,y)\) 中的点虽然是有标号的可以把它们看成是等价的。

如果 \(x+1<y\),那么 \(S(x,y)\) 中恰好有 \(|S(x,y)|-n_2\) 个节点向 \(S(x-1,y-1)\) 连边,枚举有 \(i\) 个在 \(S(x-1,y+1)\) 的点连向了 \(j\) 个在 \(S(x,y)\) 中的点,那么这 \(i\) 个点肯定包含那 \(n_2\) 个点,这 \(j\) 个点肯定包含那 \(n_1\) 个点,所以有转移:

\[\begin{aligned} dp(x,y,n_2)=&\sum_{n_1,n_2}dp(x-1,y+1,n_1){|S(x,y)|\choose n_2}H(|S(x-1,y-1)|,|S(x,y)|-n_2)\\ &\sum_{i,j}{|S(x,y)|-n_2\choose i-n_2}{|S(x-1,y+1)|-n_1\choose j-n_1}G(j,i) \end{aligned} \]

这个转移复杂度有点高的恐怖,但是发现 \(n_1,n_2\) 是比较割裂的,且因为 \(G(j,i)\) 这一项的存在所以只会和 \(j\) 有点关系,所以定义辅助数组的时候把 \(j\) 放进去就可以了(\(b(i)\) 的组合意义是知道了 \(S(x,y)\) 的连过去点数,\(S(x-1,y+1)\) 的方案数有多少):

\[a(i)=\sum_{n_1\leq i} dp(x-1,y+1,n_1){|S(x-1,y+1)|-n_1\choose j-n_1} \]

\[b(i)=\sum_j a(j)g(j,i) \]

\[\begin{aligned} dp(x,y,n_2)=&\sum_{n_2}{|S(x,y)|\choose n_2}H(|S(x-1,y-1)|,|S(x,y)|-n_2)\\ &\sum_{i}{|S(x,y)|-n_2\choose i-n_2}b(i) \end{aligned} \]

那么现在做这部分的时间复杂度就是 \(O(n^2)\) 的了。

如果 \(x+1=y\),因为无法让后面解决问题所以只有 \(dp(x,y,0)\) 这一项是有意义的,考虑 \(S(x,y)\) 中有一部分的节点会连向 \(S(x,y)\)\(S(x-1,y+1)\),这样这些点就不需要再用 \(S(x-1,y-1)\) 来解决问题了,所以我们要枚举连向 \(S(x,y),S(x-1,y+1)\) 的重复部分,记为 \(w\),设连向 \(S(x,y)\) 的点数为 \(k\),连向 \(S(x-1,y+1)\) 的点数为 \(i\),有转移:

\[\begin{aligned} dp[x][y][0]=&{|S(x,y)|\choose w}H(|S(x-1,y-1)|,n-w)F(|S(x-1,y-1)|,w)\\ &\sum_{w\leq i,k}{|S(x,y)|-w\choose i-w}b(i){|S(x,y)|-i\choose k-w}Q(k) \end{aligned} \]

\(x,y,w\) 三个变量的总枚举时间是 \(O(n)\)(因为一共才 \(n\) 个点),然后 \(i,k\) 也暴力枚举即可,时间复杂度 \(O(n^3)\)

没时间了,就写代码了,有个出题人的标程。还有 \(O(n^2)/O(n\log n)\) 的做法别问我我不会

#include <bits/stdc++.h>

using namespace std;

const int MOD = 1e9+7;

using ll = long long;

#define f first
#define s second

struct mi {
 	int v; 
 	explicit operator int() const { return v; } 
	mi() { v = 0; }
	mi(ll _v):v(int(_v%MOD)) { v += (v<0)*MOD; }
};
mi& operator+=(mi& a, mi b) { 
	if ((a.v += b.v) >= MOD) a.v -= MOD; 
	return a; }
mi& operator-=(mi& a, mi b) { 
	if ((a.v -= b.v) < 0) a.v += MOD; 
	return a; }
mi operator+(mi a, mi b) { return a += b; }
mi operator-(mi a, mi b) { return a -= b; }
mi operator*(mi a, mi b) { return mi((ll)a.v*b.v); }
mi& operator*=(mi& a, mi b) { return a = a*b; }
mi pow(mi a, ll p) { assert(p >= 0); // asserts are important! 
	return p==0?1:pow(a*a,p/2)*(p&1?a:1); }

using vi = vector<int>;
using vmi = vector<mi>;

int N,M;
vector<vi> adj;
 
vector<array<int,2>> gen_dist() { // BFS
	vector<array<int,2>> dist(N,{MOD,MOD});
	queue<pair<int,int>> q;
	auto ad = [&](int a, int b) {
		if (dist[a][b%2] != MOD) return;
		dist[a][b%2] = b; q.push({a,b});
	};
	ad(0,0);
	while (!q.empty()) {
		pair<int,int> p = q.front(); q.pop();
		for (int t: adj[p.f]) ad(t,p.s+1);
	}
	return dist;
}

mi comb[105][105]; // comb[x][y] = (x choose y)
vmi p2; // stores powers of 2

void solve() {
	cin >> N >> M;
	adj = vector<vi>(N);
	for (int i = 0; i < M; ++i) {
		int a,b; cin >> a >> b; --a,--b;
		adj[a].push_back(b), adj[b].push_back(a);
	}
	vector<array<int,2>> dists = gen_dist();
	for (array<int,2>& t: dists) 
		if (t[0] > t[1]) swap(t[0],t[1]);
	if (dists[0][1] == MOD) { // bipartite
		vi num_at_dist(N);
		for (int i = 0; i < N; ++i) 
			++num_at_dist[min(dists[i][0],dists[i][1])];
		mi ans = 1;
		for (int i = 1; i < N; ++i)
			ans *= pow(p2[num_at_dist[i-1]]-1,num_at_dist[i]);
		cout << (int)ans << "\n";
		return;
	}
	vector<vi> S(4*N,vi(4*N));
	for (array<int,2> t: dists) ++S[t[0]][t[1]];
	mi ans = 1;
	for (int sum = 1; sum < 4*N; sum += 2) {
		vmi dp(S[0][sum]+1); dp.back() = 1;
		for (int a = 1; a <= sum/2; ++a) { // solve in increasing order of a
			int b = sum-a, num = S[a][b];
			vmi dp_int(num+1);
			{ // deal with s_{a-1,b+1} to s_{a,b}
				int prev_num = S[a-1][b+1];
				for (int x = 0; x <= num; ++x) { // x in s_{a,b} with no edges at all, num-x that receive edges
					for (int y = 0; y <= prev_num; ++y) { // y nodes in s_{a-1,b+1} that need an edge
						mi tot_mul = 0;
						for (int k = 0; k <= y; ++k) { // suppose that k out of y didn't actually get an edge
							mi mul = comb[y][k]*pow(p2[prev_num-k]-1,num-x);
							if (k&1) tot_mul -= mul;
							else tot_mul += mul;
						}
						dp_int[x] += tot_mul*dp[y];
					}
					dp_int[x] *= comb[num][x];
				}
			}
			{ // deal with s_{a-1,b-1} to s_{a,b}
				dp = vmi(num+1);
				int lower_num = S[a-1][b-1];
				for (int x = 0; x <= num; ++x) { // x will not be part of such an edge
					for (int y = 0; x+y <= num; ++y) { // y urgently need an edge, num-y don't
						mi mul = comb[num-y][x]*pow(p2[lower_num]-1,num-x);
						dp[x] += mul*dp_int[y];
					}
				}
			}
		}
		// finally, deal with clique edges
		mi ans_for_layer = 0;
		int num = S[sum/2][sum/2+1];
		for (int y = 0; y <= num; ++y) { // how many ways to complete if y need an edge
			mi tot_mul = 0;
			for (int k = 0; k <= y; ++k) {
				mi mul = comb[y][k]*p2[(num-k)*(num-k+1)/2];
				if (k&1) tot_mul -= mul;
				else tot_mul += mul;
			}
			ans_for_layer += tot_mul*dp[y];
		}
		ans *= ans_for_layer;
	}
	cout << (int)ans << "\n";
}
 
int main() {
	comb[0][0] = 1;
	for (int i = 1; i < 105; ++i) {
		comb[i][0] = 1;
		for (int j = 1; j <= i; ++j) 
			comb[i][j] = comb[i-1][j]+comb[i-1][j-1];
	}
	p2 = {1}; 
	for (int _ = 0; _ < 10000; ++_) 
		p2.push_back(2*p2.back());
	int TC; cin >> TC;
	while (TC--) solve();
}

posted @ 2021-03-19 22:22  C202044zxy  阅读(73)  评论(0编辑  收藏  举报