[冲刺国赛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)\) 求出来,那么要求的函数是:
可以用无穷级数的方式去解决 \(g(g(...g(0)...))\),即:
那么暴力找到循环节就可以计算了,时间复杂度 \(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();
}