[冲刺国赛2022] 模拟赛9

被创与地震

题目描述

\(n\) 个点 \(m\) 条边的无向图,初始时所有边都没有被激活,任意两点都是不连通的。每个点 \(i\) 有点权 \(a_i\),如果一条边 \((u,v,c)\) 满足 \(u\) 联通块的 \(a\) 值和 \(+\) \(v\) 连通块的 \(a\) 值和 \(\geq c\),则可以激活边 \((u,v)\),以后都可以通过这条边。

但如果 \((u,v)\) 已经在同一条联通块内则不可以激活这条边。问最大激活边数,我们按顺序写下激活边的编号,在激活边数最大的情况下,还要最小化这个序列的字典序。

\(n\leq 10^5,m\leq 2\cdot 10^5,s_i,a_i\leq 10^6\)

解法

观察到最大激活边数和加入哪条边以及什么时候加入都无关,所以每次直接加入编号最小的可加入的边。

考虑启发式合并,每次只需要考虑从这个连通块连出去的边。我们想要取出合法的边,但是由于不知道另一边的情况所以这并不好做。考虑放宽限制,每次可以多检查一些边,但是一条边最多被检查的次数是较小的。

不妨构造一种检查方式,使得至多检查 \(O(\log s)\) 次之后这条边一定合法。可以想到检查一次之后让值域折半,可以对每条边设置一个阈值 \(lim\),如果当前连通块的权值和超过 \(lim\) 就检查这条边。

对于点 \(u\) 连出去的边 \((u,v,c)\),把 \(lim\) 设置为 \(a[u]+\lceil\frac{c-a[u]-a[v]}{2}\rceil\),表示 \(u\) 的权值必须要 \(\geq lim\) 才有可能从 \(u\) 处检查这条边,要不然检查是无意义的(因为这条边的两边都小于需要增量的一半)

在检查这条边的时候更新它的 \(lim\),时间复杂度 \(O(n\log^2n+n\log A)\)

#include <cstdio>
#include <vector>
#include <iostream>
#include <queue>
#include <set>
using namespace std;
const int M = 200005;
const int inf = 0x3f3f3f3f;
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,x[M],y[M],c[M],fa[M],a[M];
struct node
{
    int v,d,i;
    bool operator < (const node &b) const
        {return d==b.d?i<b.i:d<b.d;}
};set<node> s[M];vector<int> ans;
priority_queue<int,vector<int>,greater<int>>q;
int find(int x)
{
    if(x!=fa[x]) fa[x]=find(fa[x]);
    return fa[x];
}
void merge(int u,int v)
{
    if(s[u].size()<s[v].size()) swap(s[u],s[v]);
    for(node t:s[v]) s[u].insert(t);
    fa[v]=u;a[u]=min(a[u]+a[v],inf);
    for(auto it=s[u].begin();!s[u].empty();it=s[u].begin())
    {
        if(it->d>a[u]) break;
        int v=find(it->v),i=it->i;
        if(u==v) {s[u].erase(it);continue;}
        if(a[u]+a[v]>=c[i])
        {
            q.push(i);
            s[u].erase(it);
            continue;
        }
        int d=(c[i]-a[u]-a[v]+1)/2;
        s[u].erase(node{v,it->d,i});
        s[v].erase(node{u,it->d,i});
        s[u].insert(node{v,a[u]+d,i});
        s[v].insert(node{u,a[v]+d,i});
    }
}
signed main()
{
    freopen("earthquake.in","r",stdin);
    freopen("earthquake.out","w",stdout);
    n=read();m=read();
    for(int i=1;i<=n;i++) a[i]=read(),fa[i]=i;
    for(int i=1;i<=m;i++)
    {
        x[i]=read();y[i]=read();c[i]=read();
        if(a[x[i]]+a[y[i]]>=c[i]) q.push(i);
        else
        {
            int d=(c[i]-a[x[i]]-a[y[i]]+1)/2;
            s[x[i]].insert(node{y[i],a[x[i]]+d,i});
            s[y[i]].insert(node{x[i],a[y[i]]+d,i});
        }
    }
    while(!q.empty())
    {
        int t=q.top();q.pop();
        if(find(x[t])==find(y[t])) continue;
        ans.push_back(t);
        merge(find(x[t]),find(y[t]));
    }
    printf("%d\n",ans.size());
    for(int x:ans) printf("%d ",x);
    puts("");
}

偶耶与时光机

题目描述

数轴上有 \(2n+2\) 个点,分别是 \(0,1,2....2n,2n+1\),神 \(\tt OUYE\) 想要从 \(0\) 走到 \(2n+1\),每次他会从 \(i\) 走到 \(i+1\)

\(1,2...2n\)\(2n\) 个点被建立了传送门,一共有 \(n\) 个传送门,每个点都在且仅在一个传送门中。传送门的规则是:对于一个传送门 \((i,j)\),如果从 \(i-1\) 走到 \(i\),则会被强制传送到 \(j\);如果从 \(j-1\) 走到 \(j\),则会被强制传送到 \(i\)

现在给定 \(m\) 个整数,\(a_i\) 表示神 \(\tt OUYE\) 徒步且仅徒步经过了 \((a_i,a_i+1)\) 这些小段(区别于传送);请你构造传送门的方案满足 \(\tt OUYE\),或者是告诉 \(\tt OUYE\) 这不可能。

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

解法

首先考虑所有小段都被经过的情况,发现 \(n\) 是偶数的情况容易构造,即把相邻四个分为一组;\(n\) 为奇数的情况手玩发现无解(懒得证明了)

对于有小段没有被经过的情况,我们把点分成下列四类:

  • 左右两边的小段都被经过了,记为 A
  • 左右两边的小段都没有被经过,记为 N
  • 左边的小段被经过了,记为 L
  • 右边的小段被经过了,记为 R

A 的个数为奇数显然无解,可以现特判掉。

首先考虑 A 的个数为 \(4\) 的倍数的情况,在这种情况下,我们首先把 LR 贪心地匹配(最近的两个匹配在一起),N 之间可以任意匹配。这样 LR 可以达到跳过中间段不经过的效果,剩下的 A 可以按照相邻四个一组的方法构造。

那么 A 的个数不为 \(4\) 的个数就一定无解吗?其实我们可以把分开的 LR...LR 等效成 A...A,方法是把外层的 LR 和内层的 RL 连边,这样走到第一个 L 就会从第二个 R 出来,走到第二个 L 就会从第一个 R 出来,所以这和 A...A 是等效的。

那么现在 A 的个数可以看成 \(4\) 的倍数个,我们首先把这个含有等效 A 的组分配好。方法就是找到内部的第一个 A,然后找到外部左边的第一个 A 或者外部右边的第一个 A 然后分为一组。剩下的 A 用相邻四个一组的方法构造。

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

#include <cstdio>
#include <vector>
#include <cassert>
#include <iostream>
#include <set>
using namespace std;
const int M = 200005;
#define pii pair<int,int>
#define pb push_back
#define fi first
#define se second
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,a[M];vector<int> s;
set<int> b;vector<pii> ans;
signed main()
{
	freopen("shuttle.in","r",stdin);
	freopen("shuttle.out","w",stdout);
	n=read()<<1;m=read();
	for(int i=1;i<=m;i++) a[read()]=1;
	pii p(0,0),q(0,0);
	for(int i=1,l1=0,l2=0;i<=n;i++)
	{
		if(a[i] && a[i-1]) b.insert(i);
		else if(!a[i] && !a[i-1])
		{
			if(l1) ans.pb({l1,i}),l1=0;
			else l1=i;
		}
		else
		{
			if(!l2) {l2=i;continue;}
			if(!p.fi) p={l2,i};
			else if(!q.fi && b.size() && *b.rbegin()>p.se)
				q={l2,i};
			else ans.pb({l2,i});
			l2=0;
		}
	}
	if(b.size()&1) {puts("No");return 0;}
	if(b.size()%4==0)
	{
		if(p.fi) ans.pb(p);
		if(q.fi) ans.pb(q);
	}
	else
	{
		if(!q.fi) {puts("No");return 0;}
		ans.pb({p.fi,q.se});
		ans.pb({p.se,q.fi});
		auto i=lower_bound(b.begin(),b.end(),p.fi);
		auto j=lower_bound(b.begin(),b.end(),q.fi);
		if(i!=b.begin())
		{
			int v=*i;i--;int u=*i;
			ans.pb(make_pair(u,v));
			b.erase(u);b.erase(v);
		}
		else if(j!=b.end())
		{
			int u=*i,v=*j;
			ans.pb({u,v});
			b.erase(u);b.erase(v);
		}
		else {puts("No");return 0;}
	}
	for(int x:b) s.pb(x);
	for(int i=0;i<s.size();i+=4)
	{
		ans.pb({s[i],s[i+2]});
		ans.pb({s[i+1],s[i+3]});
	}
	puts("Yes");
	for(auto x:ans) printf("%d %d\n",x.fi,x.se);
}

杀老师与赌场

题目描述

杀老师有 \(<1\) 的钱,他想赚够一块钱买个棒棒糖。

此时正好有一个赌场,如果杀老师投入 \(x\) 的钱,有 \(p\) 的几率赚到 \(x\),有 \(1-p\) 的几率血本无归。

请问最优策略下,杀老师能吃到棒棒糖的期望是多少。杀老师初始的钱数由一个分数 \(\frac{x}{y}\) 给出。

\(x,y\leq 10^6\)

\(\tt subtask\)\(y\)\(2\) 的某个次幂。

解法

首先考虑 \(y\)\(2^k\) 的情况,最优策略是把 \(\frac{x}{y}\) 写成二进制小数,比如 \(0.010101...\),然后从最低位一路操作上来。正确性大家可以感性理解一下(因为这结论我都能发现)

计算答案考虑 \(dp\),设 \(dp[i]\) 表示操作到第 \(i\) 位可以向上进位的概率。那么答案是 \(dp[1]\),转移:

  • 如果这一位是 \(1\)\(dp[i]=dp[i+1]+p\cdot (1-dp[i+1])\)
  • 如果这一位是 \(0\)\(dp[i]=dp[i+1]\cdot p\)

对于 \(y\) 不是 \(2^k\) 的情况,可以把 \(\frac{x}{y}\) 写成循环二进制小数,当出现相同的余数是就找到的循环,所以根据鸽笼原理,循环节的长度是 \(O(y)\) 的。可以把转移写成 \(g(x)=kx+b\) 的形式,一次函数的复合规则是 \(f(g(x))=k_1(k_2x+b_2)+b_1\),我们先把非循环部分的复合函数 \(f(x)\) 和循环部分的复合函数 \(g(x)\) 求出来,那么要求的函数是:

\[f(g(g(...g(0)...))) \]

可以用无穷级数的方式去解决 \(g(g(...g(0)...))\),即:

\[g(g(...g(0)...))=b+kb+k^2b+k^3b.....=\frac{b}{1-k} \]

那么暴力找到循环节就可以计算了,时间复杂度 \(O(y)\)

#include <cstdio>
const int M = 2000005;
#define int long long
const int MOD = 1e9+7;
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,x,y,p,n,vis[M],a[M],k[M],b[M],to[M],s[M];
int qkpow(int a,int b)
{
	int r=1;
	while(b>0)
	{
		if(b&1) r=r*a%MOD;
		a=a*a%MOD;
		b>>=1;
	}
	return r;
}
int solve(int u)
{
	if(vis[u])
	{
		int K=1,B=0;
		while(1)
		{
			int v=s[n];
			K=K*k[v]%MOD;
			B=B*k[v]%MOD;
			B=(B+b[v])%MOD;
			if(s[n]==u) break;
			n--;
		}
		return B*qkpow(MOD+1-K,MOD-2)%MOD;
	}
	s[++n]=u;vis[u]=1;
	return (b[u]+k[u]*solve(to[u]))%MOD;
}
void work()
{
	x=read();y=read();
	p=read()*qkpow(read(),MOD-2)%MOD;
	vis[0]=0;
	for(int i=1;i<y;i++)
	{
		vis[i]=0;
		if(i*2<y) to[i]=i*2,k[i]=p,b[i]=0;
		else to[i]=i*2-y,k[i]=(MOD+1-p)%MOD,b[i]=p;
	}
	printf("%d\n",solve(x));
}
signed main()
{
	freopen("bat.in","r",stdin);
	freopen("bat.out","w",stdout);
	T=read();
	while(T--) work();
}
posted @ 2022-06-21 19:27  C202044zxy  阅读(238)  评论(3编辑  收藏  举报