Deltix Round, Spring 2021

D. Love-Hate

题目描述

点此看题

\(n\) 个长度为 \(m\) 的二进制串,为 \(1\) 的串至多有 \(p\) 个。

试从中选出 \(\lceil\frac{n}{2}\rceil\) 个串,使得他们并集为 \(1\) 个位个数最大。

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

解法1

暴力出奇迹!我们搜索可能的答案,计算有多少个串能选进去。

每次加入可能成为答案的数位(至少出现 \(\lceil\frac{n}{2}\rceil\) 次),然后用 \(\tt bitset\) 维护能选的串的数量。

解法2

一个更加稳定的做法,我们考虑充分利用选串个数为 \(\lceil\frac{n}{2}\rceil\) 这个条件,考虑对于一个串他有 \(\frac{1}{2}\) 的概率不出现在答案里面,而如果一个串出现在答案里面我们一定能用它算出答案,答案一定是他的子集。

那么我们随机 \(20\) 个下标,就有 \((\frac{1}{2})^{20}\) 的概率得到正确答案。

确定下标之后就好做了,我们看有多少个人的子集是 \(s\),可以子集枚举 \(O(3^p+p\cdot n)\) 算出。

你发现这是个 \(\tt FWT\) 正变换的过程,所以也可以 \(O(p\cdot 2^p+p\cdot n)\) 算出。

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <vector>
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,p,mx,ans[65],a[M][65],bit[1<<15],cnt[1<<15];
int random(int x)
{
	return (rand()*rand()+rand())%x;
}
void work(int x)
{
	vector<int> v;
	for(int i=1;i<=m;i++)
		if(a[x][i]) v.push_back(i);
	memset(cnt,0,sizeof cnt);
	for(int i=1;i<=n;i++)
	{
		int msk=0;
		for(int j=0;j<v.size();j++)
			if(a[i][v[j]]) msk|=(1<<j);
		cnt[msk]++;
	}
	for(int i=0;i<(1<<p);i++)
		for(int j=i;;j=(j-1)&i)
		{
			if(i!=j) cnt[j]+=cnt[i];
			if(j==0) break;
		}
	for(int i=0;i<(1<<p);i++)
		if(cnt[i]>=(n+1)/2 && bit[i]>mx)
		{
			mx=bit[i];
			memset(ans,0,sizeof ans);
			for(int j=0;j<p;j++)
				if(i&(1<<j)) ans[v[j]]=1;
		}
}
signed main()
{
	n=read();m=read();p=read();
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
			scanf("%1d",&a[i][j]);
	}
	for(int i=1;i<(1<<p);i++)
		bit[i]=bit[i>>1]+(i&1);
	for(int i=1;i<=20;i++)
	{
		int x=random(n)+1;
		work(x);
	}
	for(int i=1;i<=m;i++)
		printf("%d",ans[i]);
}

E. Crypto Lights

题目描述

点此看题

\(n\) 栈灯,每次随机选择一盏未点亮的灯点亮,如果存在连续 \(k\) 栈灯亮了两个及以上就跳出,求点亮灯个数的期望。

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

解法

好久没做过这么纯粹的期望题了,但是不会

拆贡献是算期望最重要的方法,具体的,对于本题我们计算点亮 \(i\) 栈灯,并且当前状态合法的概率,对所有 \(i\) 的概率求和 \(+1\) 就是期望。

可以先算方案数,我们选择 \(i\) 个灯亮,因为不能把不合法的方案算进去,所以我们计算方式是先强制每两个 \(1\) 中有 \(k-1\)\(0\),然后剩下的 \(0\) 任意插入 \(1\) 的间隙中,这相当于 \(n-(i-1)(k-1)\) 个位置中选择 \(i\)\(1\),即 \({n-(i-1)(k-1)\choose i}\)

因为有顺序问题所以还要略微修正一下,因为这 \(i\) 个灯可以任意顺序点亮所以要乘上 \(i!\),因为我们的考虑不是完全的,所以每一种方案实际上代表着 \((n-i)!\) 种最终方案。即使有些方案会中途夭折,我们还是可以把最终总方案当成 \(n!\)只不过有些方案的贡献计算变化罢了,所以要乘上 \((n-i)!\) 再除以 \(n!\)

\[ans=1+\frac{1}{n!}\sum_{i=1}^n{n-(i-1)(k-1)\choose i}\cdot i!\cdot (n-i)! \]

#include <cstdio>
const int M = 100005;
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,k,ans,fac[M],inv[M];
void init(int n)
{
	fac[0]=inv[0]=inv[1]=1;
	for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%MOD;
	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;
}
int C(int n,int m)
{
	if(n<m) return 0;
	return fac[n]*inv[m]%MOD*inv[n-m]%MOD;
}
signed main()
{
	T=read();init(1e5);
	while(T--)
	{
		n=read();k=read();ans=0;
		for(int i=1;i<=n;i++)
			ans=(ans+C(n-(i-1)*(k-1),i)*fac[i]%MOD*fac[n-i])%MOD;
		ans=(ans*inv[n]+1)%MOD;
		printf("%lld\n",ans);
	}
}

F. Favorite Game

题目描述

点此看题

在二维平面内,有 \(n\) 个传送塔和 \(m\) 个地标,初始时阿七可以降落在任意地点,每秒可以选择不动,或者向四周移动一步。如果阿七到过某个传送塔,就会将其激活,以后可以瞬间移动到这个传送塔的位置。

每个地标有指定地点和指定时间 \(t_i\),如果阿七正好在 \(t_i\) 时间到达这个地标就可以完成一个刺杀任务,最大化任务完成数。

\(n\leq 14,m\leq 100,1\leq x,y\leq 10^6,1\leq t_i\le10^9\),保证所有位置不同。

解法

首先把所有地标按指定时间升序排序,这样就确定了 \(dp\) 的转移顺序。

可以定义出最朴素的状态,设 \(dp(s,i,j)\) 表示阿七到达的传送塔集合状压为 \(s\),任务完成总数为 \(i\),现在的地点是 \(j\)(可能是地标也可能是传送塔)的最小消耗时间,但是状态数就有了 \(O(2^nm^2)\) 直接起飞,我们不妨来搞一些 \(\tt observations\)

  • 重要的量有四个:到达的传送塔集合,任务完成总数,现在所处地点,当前时间,看上去三维 \(dp\) 是必须的。
  • 如果阿七在传送塔,因为可以瞬移,所以我们不用关心阿七在哪个传送塔,那么就只剩了三个量。
  • 如果阿七在地标,因为刺杀任务的时间指定是 \(t_i\),所以我们不用关心时间,那么也只剩了三个量。
  • 所有的情况可以分为阿七在传送塔和阿七在地标。

综合以上观察,我们得到了优化的思路:通过类似分类讨论的方法来减少状态量,给 \(dp\) 降维

上面是我自己的思考,现在就能顺理成章地得到题解的做法:设 \(f[s][i]\) 表示到达的传送塔集合为 \(s\),任务完成总数为 \(i\),现在处于传送塔的最小时间;设 \(g[s][i]\) 表示到达传送塔集合为 \(s\),现在处于地标 \(i\) 的最大任务完成总数,来写转移:

  • \(w[s][i]\) 表示到达传送塔集合为 \(s\),从某个传送塔到地点 \(i\) 的最小时间,这个可以预处理方便转移。
  • 从某个传送塔出发到一个新的传送塔:\(f[s\oplus j][i]\leftarrow f[s][i]+w[s][j]\)
  • 从某个传送塔出发到一个新的地标:\(g[s][j]\leftarrow i+1\),当满足 \(f[s][i]+w[s][j]\leq t[j]\) 时转移。
  • 从地标出发到一个新的传送塔:\(f[s\oplus j][g[s][i]]\leftarrow t[i]+\min(w[s][j],dis[i][j])\)
  • 从地标到下一个地标:\(g[s][j]\leftarrow g[s][i]+1\),当满足 \(\min(w[s][j],dis(i,j))+t[i]\leq t[j]\) 时转移。

实现中的转移顺序应该是:枚举 \(s\),先进行 \(f\) 的转移,再进行 \(g\) 的转移。

时间复杂度 \(O(2^nm^2)\),有一些小细节可以参考代码注释。

#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 120;
const int N = 1<<14;
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,ans,f[N][M],g[N][M],w[N][M];
//f[s][i]:stand for having reached tower s,visited i landmarks,now in a tower
//g[s][i]:stand for having reached tower s,now in a landmark
struct node
{
	int x,y,t;
	bool operator < (const node &r) const
	{
		return t<r.t;
	}
}a[M];
int Abs(int x) {return x>0?x:-x;}
int get(node a,node b)
{
	return Abs(a.x-b.x)+Abs(a.y-b.y);
}
signed main()
{
	n=read();m=read();
	for(int i=0;i<n;i++)
		a[i].x=read(),a[i].y=read();
	for(int i=n;i<n+m;i++)
		a[i].x=read(),a[i].y=read(),a[i].t=read();
	sort(a+n,a+n+m);
	for(int s=0;s<(1<<n);s++)
	{
		for(int i=0;i<n+m;i++)
		{
			f[s][i]=w[s][i]=inf;g[s][i]=-inf;
			for(int j=0;j<n;j++)
				if(s&(1<<j))
					w[s][i]=min(w[s][i],get(a[i],a[j]));
		}
	}
	for(int i=0;i<n;i++) f[1<<i][0]=0;
	for(int i=0;i<m;i++) g[0][i]=1;
	for(int s=0;s<(1<<n);s++)
	{
		//transitions with f
		for(int i=0;i<=m;i++)
			if(f[s][i]<inf)
			{
				for(int j=0;j<n;j++)
					if(!(s&(1<<j)))
						f[s|(1<<j)][i]=
						min(f[s|(1<<j)][i],f[s][i]+w[s][j]);
				for(int j=0;j<m;j++)//j will not be a landmark that is visited 
					if(f[s][i]+w[s][j+n]<=a[j+n].t)
						g[s][j]=max(g[s][j],i+1);
			}
		//transitions with g
		for(int i=0;i<m;i++)
			if(g[s][i]>=0)
			{
				for(int j=0;j<n;j++)
					if(!(s&(1<<j)))
						f[s|(1<<j)][g[s][i]]=min(f[s|(1<<j)][g[s][i]]
						,a[i+n].t+min(get(a[i+n],a[j]),w[s][j]));
				for(int j=i+1;j<m;j++)//to guarantee j is not visited
					if(min(get(a[i+n],a[j+n]),w[s][j+n])+a[i+n].t<=a[j+n].t)
						g[s][j]=max(g[s][j],g[s][i]+1);
				ans=max(ans,g[s][i]);
			}
	}
	printf("%d\n",ans);
}

G. Try Booking

题目描述

点此看题

大保\(\tt J\)发廊近期的生意火爆,对于 \(n\) 天的营业期,大保收到了 \(m\) 个订单,表示在 \([l_i,r_i]\) 让阿七剪头发。大保会按收到订单的顺序处理,如果当前订单和以前订单没有冲突,就接这个订单。

大保想知道如果忽略 \(r_i-l_i+1<x\) 的订单最后阿七的营业天数是多少,对于所有 \(x\in[1,n]\) 都要求出答案。

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

解法

我真的没观察出什么性质,当前订单的选取对后面的影响实在是太大了。

有一个重要的 \(\tt observation\):如果我们每次都能直接找到下一个接的订单,那么总共接订单的数量是 \(\sum_{i=1}^n\frac{n}{i}=O(n\log n)\)

也就是说只要我们能快速找到下一个订单是谁复杂度就对了,由于是一个二维偏序问题,那么直接上一个树套树即可,知道订单之后就把当前区间分成两半递归下去,相当于一个分治查的过程。

时间复杂度 \(O(n\log^3n)\),原谅我没有写代码。

H. Hopping Around the Array

题目描述

点此看题

小飞在长度为 \(n\) 的数组上面玩游戏,位置 \(i\) 有一个跳跃距离 \(a_i\),小飞可以选择通过一步跳跃降落到 \((i,i+a_i]\) 的任意一个地点。但是它觉得直接跳太累了,所以阿七可以帮助它移走任意 \(k\) 个位置,剩下的位置会重新编号,但是 \(a\) 不会改变。

一共有 \(q\) 次游戏,每次从 \(l\) 处出发,\(r\) 处结束,能移走 \(k\) 个位置,但是不能移走起点和终点。作为世界上最强的飞鸡,小飞想知道最小跳跃次数。

\(1\leq n,q\leq 20000,1\leq l\leq r\leq n,0\leq k\leq \min(30,r-l)\)

解法

首先考虑一个简化的问题,如果没有移走位置的操作怎么最小化飞行次数。

你肯定会说 \(dp\),这固然很好,但是这种方法无法扩展到本题的情况。不妨贪心一点考虑,肯定不能无脑跳最远的地方,但是我们可以让我们的跳跃范围最远,也就是我们找到 \(a_i+i\) 最大的位置直接跳它,这种贪心在最后一步的跳远除外都是使用的,而且它的可扩展性显然优于 \(dp\)

然后考虑有了移走操作怎么办,移走操作的本质是增加跳跃范围,也就是说只有你想跳到 \(a[i]+i+x\) 的时候你才会移走 \(x\) 个格子,否则无需移走格子也能跳到你想要的地方。

如果询问只有一组现在是可以解决的,但是本题是多组询问。回忆我省选的惨痛经历,这种既有步数移动又是多组询问的问题是和倍增很搭的,因为 \(k\) 很小而且需要分配移动操作所以可以把他加入状态中,那么不难定义出倍增数组 \(f[i][j][k]\) 表示从位置 \(i\) 开始跳 \(2^j\) 步,移走 \(k\) 个格子能到达的最远范围

考虑转移,首先要枚举前一半用掉的删除次数是 \(x\),那么用 \(st\) 表找到 \([i,f[i][j-1][x]]\) 这个范围中最大 \(a_p+p\)\(p\),然后就可以从 \(p\) 完成后半段的跳跃,最远的范围就是 \(f[p][j-1][k-x]\),这样就完成了 \(f[i][j][k]\) 的转移。我们初始化 \(f[i][0][x]=i+a[i]+x\) 即可。

现在考虑怎么用这个倍增数组回答询问,大体的思路是用 \(\tt lca\) 后半段的那种方法,我们多跳 \(2^x\) 看会不会跳到 \(r\),如果没有跳到 \(r\) 了那么就把 \(2^j\) 计入 \(ans\),最后把 \(ans+1\) 输出即可。

由于我们要分配移动次数所以我们定义 \(g[i]\) 表示从 \(l\) 开始跳若干步,用掉的移动次数是 \(i\) 的最远范围,转移就枚举新跳的 \(2^x\) 步中用掉了移动次数 \(j\),找到 \([l,g[i]]\) 中最大 \(p+a_p\)\(p\),用 \(f[p][x][j]\) 转移到辅助数组 \(tmp[i+j]\) 中即可。

注意要限制不能跳出 \(n\),要不然下标会有问题,时间复杂度 \(O((n+q)\cdot k^2\cdot log n)\)

#include <cstdio>
#include <iostream>
using namespace std;
const int M = 20005;
const int N = 31;
#define make make_pair
#define pii pair<int,int>
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,q,a[M],lg[M],f[M][20][N],g[N],tmp[N];pii dp[M][20];
//f[i][j][k]:stand for the max (l+al) can reach from i through 2^j jumps and k delete 
int ask(int l,int r)//ask the l of max (l+al) 
{
	int k=lg[r-l+1];
	return max(dp[l][k],dp[r-(1<<k)+1][k]).second;
}
void init()
{
	for(int j=1;(1<<j)<=n;j++)//st
		for(int i=1;i+(1<<j)-1<=n;i++)
			dp[i][j]=max(dp[i][j-1],dp[i+(1<<j-1)][j-1]);
	for(int i=1;i<=n;i++)
		for(int j=0;j<N;j++)
			f[i][0][j]=min(n,i+a[i]+j);//this "min" is necessary
	for(int j=1;(1<<j)<=n;j++)
		for(int i=1;i<=n;i++)//please don't limit the range of i
			for(int k=0;k<N;k++)
				for(int x=0;x<=k;x++)
				{
					int t=ask(i,f[i][j-1][x]);//find the position
					f[i][j][k]=max(f[i][j][k],f[t][j-1][k-x]);
				}
}
signed main()
{
	n=read();q=read();
	for(int i=1;i<=n;i++)
	{
		a[i]=read();
		dp[i][0]=make(min(n,a[i]+i),i);
		if(i>1) lg[i]=lg[i>>1]+1;
	}
	init();
	while(q--)
	{
		int l=read(),r=read(),k=read(),ans=0;
		if(l==r) {puts("0");continue;}
		if(l+a[l]+k>=r) {puts("1");continue;}
		for(int i=0;i<=k;i++) g[i]=l;
		for(int x=15;x>=0;x--)
		{
			if((1<<x)>n) continue;
			for(int i=0;i<=k;i++) tmp[i]=g[i];
			for(int i=0;i<=k;i++)
			{
				int t=ask(l,g[i]);
				for(int j=0;i+j<=k;j++)
					tmp[i+j]=max(tmp[i+j],f[t][x][j]);
			}
			int fl=0;
			for(int i=0;i<=k;i++)
				if(tmp[i]>=r) fl=1;
			if(fl) continue;
			for(int i=0;i<=k;i++) g[i]=tmp[i];
			ans+=(1<<x);
		}
		printf("%d\n",ans+1);
	}
}
posted @ 2021-06-02 17:08  C202044zxy  阅读(213)  评论(0编辑  收藏  举报