Educational Codeforces Round 111 (Rated for Div. 2)

D. Excellent Arrays

题目描述

点此看题

对于一个长度为 \(n\) 的数组 \(a\),定义 \(F(a)\) 为满足 \(1\leq i<j\leq n\)\(a_i+a_j=i+j\) 的二元组个数。

求有多少满足下列条件的数列 \(a\)

  • 对于所有 \(a_i\),满足 \(a_i\not=i\)
  • \(l\leq a_i\leq r\)
  • \(F(a)\) 是所有长度为 \(n\) 的合法数列中的最大权值。

\(n\leq 2\cdot 10^5\)

解法

我们考虑什么情况下 \(a_i+a_j=i+j\) 的对数最大化,先移项,设 \(k_i=a_i-i\),那么问题是满足 \(k_i=-k_j\) 的数对个数,所有 \(a_i\not=i\) 可以翻译成 \(k_i\not=0\),那么最大权值一定是一半的数拿了正的 \(k\),一半的数拿了负的 \(k\)

值域的限制在 \(a_i\) 上面,我们考虑把限制转到 \(k\) 上面,我们先考虑什么情况每个数既能选正又能选负,如果选负 \(1\) 是最容易爆出 \(l\) 的,如果选正 \(n\) 是最容易爆出 \(r\) 的,那么有下列的不等式:

\[-k\geq l-1,k\leq r-n \]

那么当所有数都能任选的时候 \(k\)\(\min(l-1,r-n)\) 种合法取值,再乘上 \({n\choose n/2}\) 即可(\(n\) 为奇还要考虑 \({n\choose n/2+1}\)

现在如果 \(k\) 增大,有数就只能选正或者选负了,而且只能选正的一定是一段前缀,只能选负的一定是一段后缀,我们可以把端点算出来,设 \([lg,rg]\) 表示现在能任选的端点:

\[-k\geq l-lg,k\leq r-rg\rightarrow lg\geq l+k,rg\geq r-k \]

在这些任选的数中选出为负的即可,因为 \(k\) 增大 \(n\) 左右组合数方案数就是 \(0\) 了,所以时间复杂度 \(O(n)\)

总结

两个点的限制,把有关量拆到等号两边。算方案时,考虑何处最先去到端点值。

#include <cstdio>
#include <iostream>
using namespace std;
const int M = 200005;
const int MOD = 1e9+7;
#define int long long
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,l,r,ans,fac[M],inv[M];
void init(int n)
{
	fac[0]=inv[0]=inv[1]=1;
	for(int i=2;i<=n;i++) inv[i]=inv[MOD%i]*(MOD-MOD/i)%MOD;
	for(int i=2;i<=n;i++) inv[i]=inv[i-1]*inv[i]%MOD;
	for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%MOD;
}
int C(int n,int m)
{
	if(n<0 || m<0 || n<m) return 0;
	return fac[n]*inv[m]%MOD*inv[n-m]%MOD;
}
void work()
{
	n=read();l=read();r=read();
	m=n/2;ans=0;
	int t=min(1-l,r-n);
	ans=t*C(n,m)%MOD;
	if(n%2) ans=(ans+t*C(n,m+1))%MOD;
	for(int k=t+1;;k++)
	{
		int lg=max(1ll,k+l),rg=min(n,r-k);
		if(lg>rg+1) break;//attention
		ans=(ans+C(rg-lg+1,m-(lg-1)))%MOD;
		if(n%2) ans=(ans+C(rg-lg+1,m+1-(lg-1)))%MOD;
	}
	printf("%lld\n",(ans+MOD)%MOD);
}
signed main()
{
	T=read();init(2e5);
	while(T--) work();
}

E. Stringforces

题目描述

点此看题

给一个长度为 \(n\) 的字符集为 \(k\) 的串,把问号替换成小写字符,使得每个字符最长连续段的最小值最大。

\(n\leq 2\cdot 10^5,k\leq 17\)

解法

不难想到二分答案,检查的时候题目都告诉你了要状压(\(k\leq 17\)),那么就想状压呗。

你可能不知道状压要干什么,可以逆向思维,状压的在这道题的功能应该是把枚举阶乘的 \(n!\) 降为 \(2^{n}\),这说明这道题要枚举顺序,枚举什么顺序呢?枚举让每个字符合法的顺序,既然都有顺序了我们从左到右的让字符合法即可。

\(g[i][j]\) 表示从位置 \(i\) 开始要到哪里才能让字符 \(j\) 合法,这个可以简单预处理。

\(dp[s]\) 表示让字符集合 \(s\) 合法的最小右端点,转移:\(dp[s|c]\leftarrow g[dp[s]][c]\)

正向的思维过程是考虑枚举字符的顺序,然后从左到右把字符放进去。

总结

考试时候傻了,以为枚举顺序之后要贪心放进最好的位置中。其实是我对枚举的理解出了问题,枚举就是考虑所有情况取其最优,所以枚举的东西就不用贪心了,思维过程是:先枚举,以 \(dp\) 代替枚举。

#include <cstdio>
#include <iostream>
using namespace std;
const int M = 200005;
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,k,ls[20],p[M][20],dp[1<<20];char s[M];
int check(int d)
{
	for(int i=0;i<k;i++) p[n][i]=ls[i]=n+1;
	for(int i=n-1;i>=0;i--)
	{
		int mi=n;
		if(s[i]!='?') ls[s[i]-'a']=i;
		//这一段中都不能有其他字符 
		//考虑前缀和后缀最小值
		//不行只能继承上一层 
		for(int j=0;j<k;j++)
		{ 
			if(i+d<=mi) p[i][j]=i+d;
			else p[i][j]=p[i+1][j];
			mi=min(mi,ls[j]);
		}
		mi=n;
		for(int j=k-1;j>=0;j--)
		{
			if(i+d>mi) p[i][j]=p[i+1][j];
			mi=min(mi,ls[j]);
		}
	}
	dp[0]=0;
	for(int s=1;s<(1<<k);s++)
	{
		dp[s]=n+1;
		for(int i=0;i<k;i++)
			if((s&(1<<i)) && dp[s^(1<<i)]<n)
				dp[s]=min(dp[s],p[dp[s^(1<<i)]][i]);
	}
	return dp[(1<<k)-1]<=n;
}
signed main()
{
	n=read();k=read();
	scanf("%s",s);
	int l=1,r=n,ans=0;
	while(l<=r)
	{
		int mid=(l+r)>>1;
		if(check(mid))
			ans=mid,l=mid+1;
		else r=mid-1;
	}
	printf("%d\n",ans);
}

F. Jumping Around

题目描述

点此看题

\(n\) 个石头,第 \(i\) 个石头的位置是 \(a_i\),青蛙可以跳到与当前石头距离 \(\in[d-k,d+k]\) 的石头,初始时在石头 \(s\),你知道 \(d\),每次询问给你 \(k\)\(t\),问你是否能在此种情况下跳到石头 \(t\)

\(n,q\leq 2\cdot 10^5\)

解法

题目误导性极强,他给定了起点,我以为正解一定要从这个起点去扩展,结果根本不需要定起点的条件。所以思维要发散,敢于把以前想的全部推倒重来。

题目的重中之重是 \(k\),你发现 \(k\) 越大每个点能跳到的范围越大,\(u\) 能到 \(v\) 当且仅当 \(k\) 正好到某个值的时候,我们把这个值看成边的权值,转化成图论模型后,问题是问 \(s\)\(t\) 的连通性,所以可以考虑最小生成树,找到路径上权值最大的边即可。

关键是建出最小生成树,权值的秘密在点所以考虑 \(\tt Borůvka\) 算法(这个我不展开)。维护一个 \(\tt set\),考虑到当前连通块后把所有点从 \(\tt set\) 中拿掉,然后枚举点 \(\tt lower\_bound\) 找权值最小的连出去的边,做完以后再插入回 \(\tt set\) 中即可,时间复杂度 \(O(n\log^2n)\)

总结

边权和点相关的最小生成树用 \(\tt Borůvka\) 算法。

#include <cstdio>
#include <vector>
#include <algorithm>
#include <map>
#include <set>
using namespace std;
const int M = 200005;
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,q,s,d,a[M],mx[M],fa[M];map<int,int> mp;
struct node
{
	int v,c;
};vector<node> g[M];
int Abs(int x)
{
	return x>0?x:-x;
}
int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}
void solve()
{
	set<int> s;vector<int> v[M];
	for(int i=1;i<=n;i++)
	{
		v[find(i)].push_back(i);
		s.insert(a[i]);
	}
	for(int i=1;i<=n;i++)
	{
		if(!v[i].size()) continue;
		for(int x:v[i]) s.erase(a[x]);
		int mn=1e9,p=0,q=0;
		for(int x:v[i])
		{
			for(int dx:{-d,d})
			{
				set<int>::iterator it=s.lower_bound(a[x]+dx);
				if(it!=s.end())
				{
					int val=Abs(Abs(a[x]-(*it))-d);
					if(val<mn) mn=val,p=x,q=mp[*it];
				}
				if(it!=s.begin())
				{
					it--;
					int val=Abs(Abs(a[x]-(*it))-d);
					if(val<mn) mn=val,p=x,q=mp[*it];
				}
			}
		}
		if(mn<1e9 && find(p)!=find(q))
		{
			fa[find(p)]=find(q);m--;
			g[p].push_back(node{q,mn});
			g[q].push_back(node{p,mn});
		}
		for(int x:v[i]) s.insert(a[x]);
	}
}
void dfs(int u,int fa)
{
	for(node x:g[u])
	{
		if(x.v==fa) continue;
		mx[x.v]=max(x.c,mx[u]);
		dfs(x.v,u);
	}
}
signed main()
{
	n=m=read();q=read();s=read();d=read();
	for(int i=1;i<=n;i++)
		a[i]=read(),fa[i]=i,mp[a[i]]=i;
	while(m>1) solve();
	dfs(s,0);
	while(q--)
	{
		int u=read(),k=read();
		if(mx[u]<=k) puts("YES");
		else puts("NO");
	}
}
posted @ 2021-07-16 22:37  C202044zxy  阅读(69)  评论(0编辑  收藏  举报