Educational Codeforces Round 146 (Rated for Div. 2)

发布时间 2023-04-13 21:30:39作者: 空気力学の詩

Preface

补题ing

值得一提的时补这场的时候先是遇上了CF的12小时大维护,后面又遇到了评测机崩了测不了也是有点有意思的说


A. Coins

傻逼题,首先考虑\(2|n\)时一定有解\(x=\frac{n}{2},y=0\),否则若\(2\nmid n\and 2|k\)则由裴蜀定理知此时一定无解

否则\(y\)必为奇数,我们令\(x=\frac{n-k}{2},y=1\),则只要检验\(n,k\)的大小关系即可

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
int t; long long k,n;
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	for (scanf("%d",&t);t;--t)
	{
		if (scanf("%lld%lld",&n,&k),n%2==0) { puts("YES"); continue; }
		if (k%2==0) puts("NO"); else puts(n>=k?"YES":"NO"); 
	}
	return 0; 
}

B. Long Legs

一眼题,首先考虑枚举最后\(m\)的值\(x\),则贪心地肯定把步数长的跳放在最后

因此容易列出当前答案的式子其实就是\(x-1+\lceil\frac{a}{x}\rceil+\lceil\frac{b}{x}\rceil\),只要求其最小值即可

考虑到如果我们不考虑上取整,把定义域改成实数域,则这个式子就是个典型的对勾函数,在\(x=\sqrt{ab}\)时取得最小值

因此我们知道当枚举的\(x\)到这个级别的时候就差不多能看出答案了,这里我结合数据组数选择枚举到\(10^5\)即可通过

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
int t,a,b;
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	for (scanf("%d",&t);t;--t)
	{
		RI i; scanf("%d%d",&a,&b); int ans=1e9;
		for (i=1;i<=100000;++i) ans=min(ans,i-1+(a+i-1)/i+(b+i-1)/i);
		printf("%d\n",ans);
	}
	return 0; 
}

C. Search in Parallel

傻逼贪心题,首先给\(r_i\)从大到小排序后,先处理掉大的一定最优,然后放在哪里显然也就是比较一下选代价小的那个

#include<cstdio>
#include<iostream>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
const int N=200005;
struct Data
{
	int r,id;
	friend inline bool operator < (const Data& A,const Data& B)
	{
		return A.r>B.r;
	}
}a[N]; int t,n,s1,s2,A[N],B[N],cnt1,cnt2;
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	for (scanf("%d",&t);t;--t)
	{
		RI i; for (scanf("%d%d%d",&n,&s1,&s2),i=1;i<=n;++i)
		scanf("%d",&a[i].r),a[i].id=i; sort(a+1,a+n+1);
		for (cnt1=cnt2=0,i=1;i<=n;++i)
		if ((cnt1+1)*s1<(cnt2+1)*s2) A[++cnt1]=a[i].id; else B[++cnt2]=a[i].id;
		for (printf("%d ",cnt1),i=1;i<=cnt1;++i) printf("%d ",A[i]); putchar('\n');
		for (printf("%d ",cnt2),i=1;i<=cnt2;++i) printf("%d ",B[i]); putchar('\n');
	}
	return 0; 
}

D. Balancing Weapons

刚开始想了个算法码了100多行假了,后来发现是我想复杂了

首先我们肯定可以让最后每个数变成\(\operatorname{lcm} f_i\),因此答案为\(n\)的情况是一定可得的

否则我们考虑至少存在一个点不会改变,我们枚举不改变的点\(p_i\),不难发现此时其它点要变一定会变成距离\(p_i\)最近的那两个数

因此换句话说此时所有取值的可能其实是\(O(n)\)级别的,我们把所有这些可能的取值排个序,然后two pointers找出所有极大的合法区间并验证即可

具体实现可以看代码,总复杂度\(O(n^2\log n)\)

#include<cstdio>
#include<iostream>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
const int N=3005;
struct Data
{
	long long v; int tp,id;
	inline Data(const long long& V=0,CI TP=0,CI ID=0)
	{
		v=V; tp=TP; id=ID;
	}
	friend inline bool operator < (const Data& A,const Data& B)
	{
		return A.v<B.v;
	}
}rst[N*3]; int t,n,k,ans,cnt,f[N],d[N],bkt[N]; long long p[N];
inline int solve(const long long& tar)
{
	RI i,j; int tot=0,sum=0,ret=n;
	auto add=[&](const Data& it)
	{
		if (sum+=it.tp,++bkt[it.id]==1) ++tot;
	};
	auto del=[&](const Data& it)
	{
		if (sum-=it.tp,--bkt[it.id]==0) --tot;
	};
	for (cnt=0,i=1;i<=n;++i)
	{
		bkt[i]=0; rst[++cnt]=Data(p[i],1,i); long long tmp=tar/f[i]*f[i];
		if (tmp>0) rst[++cnt]=Data(tmp,0,i); rst[++cnt]=Data(tmp+f[i],0,i);
	}
	for (sort(rst+1,rst+cnt+1),i=j=1;i<=cnt;++i)
	{
		while (j<i&&rst[i].v-rst[j].v>k) del(rst[j++]);
		if (add(rst[i]),tot==n) ret=min(ret,n-sum);
	}
	return ret;
}
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	for (scanf("%d",&t);t;--t)
	{
		RI i; for (scanf("%d%d",&n,&k),i=1;i<=n;++i) scanf("%d",&f[i]);
		for (i=1;i<=n;++i) scanf("%d",&d[i]),p[i]=1LL*f[i]*d[i];
		for (ans=n,i=1;i<=n;++i) ans=min(ans,solve(p[i]));
		printf("%d\n",ans);
	}
	return 0; 
}

E. Chain Chips

真是好久没写过DDP(动态DP)的题目了,上次见到可能还是在NOIP2018的D2T3了(乐)

首先吐槽一下做这题的时候由于CF崩了就找了个简述的题面,然后里面说的是\(n\)个点\(n-1\)条边而没说是链,所以刚开始一直在想树上的做法,结果后来没想法再一看题目发现是链

不过其实两者的转化都一样,都是最小权值边覆盖问题,即我们选出若干条边,使得每个点都会被至少一条边覆盖,然后找一种总权值和最小的方案

证明也很简单,如果给出了一种所述的方案,显然很容易构造一组解(通过只交换相邻的元素即可)

而反过来如果存在某个点没有任何一条边覆盖了它,则它一定不可能换成别的芯片,因此原来结论成立

那么我们很容易想到一个naive的DP,设\(f_{i.0/1}\)表示选完了前\(i\)条边,其中第\(i\)条边选/不选的最小代价和,则显然有转移:

\[f_{i,0}=f_{i-1,1}+a_i\\ f_{i,1}=\min(f_{i-1,0},f_{i-1,1})+a_i \]

然后我们把这个转移写成DDP的矩阵形式有:

\[[f_{i-1,0},f_{i-1,1}]\times \left[\begin{matrix} \infty,a_i\\ 0,a_i \end{matrix}\right]=[f_{i,0},f_{i,1}] \]

然后用线段树维护一下即可,总复杂度\(O(n\log n)\)

不过要注意一个地方,在维护这种最值的转移矩阵时要写成右乘的形式不然会出问题,调了样例半天才发现

如果一定要写成用列向量左乘转移矩阵的话就要从右边转移到左边,这点一定要注意,在序列上还好,在树剖的时候就很容易搞错顺序

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=200005;
const long long INF=5e17;
struct Matrix
{
	int n,m; long long mat[2][2];
	inline Matrix(CI N=2,CI M=2)
	{
		n=N; m=M; for (RI i=0,j;i<n;++i) for (j=0;j<m;++j) mat[i][j]=INF;
	}
	inline long long* operator [] (CI x) { return mat[x]; }
	inline void init(CI mv)
	{
		mat[0][0]=INF; mat[1][0]=0; mat[0][1]=mat[1][1]=mv;
	}
	friend inline Matrix operator * (Matrix A,Matrix B)
	{
		Matrix C(A.n,B.m); for (RI i=0,j,k;i<C.n;++i)
		for (j=0;j<C.m;++j) for (k=0;k<A.m;++k)
		C[i][j]=min(C[i][j],A[i][k]+B[k][j]); return C;
	}
}; int n,a[N],q,k,x;
class Segment_Tree
{
	private:
		Matrix t[N<<2];
		inline void pushup(CI now)
		{
			t[now]=t[now<<1]*t[now<<1|1];
		}
	public:
		#define TN CI now=1,CI l=2,CI r=n-1
		#define LS now<<1,l,mid
		#define RS now<<1|1,mid+1,r
		inline void build(TN)
		{
			if (l>r) return; if (l==r) return t[now].init(a[l]);
			int mid=l+r>>1; build(LS); build(RS); pushup(now);
		}
		inline void updata(CI pos,TN)
		{
			if (l>r) return; if (l==r) return t[now].init(a[l]); int mid=l+r>>1;
			if (pos<=mid) updata(pos,LS); else updata(pos,RS); pushup(now);
		}
		inline Matrix query(void)
		{
			if (2>n-1) return Matrix(); return t[1];
		}
		#undef TN
		#undef LS
		#undef RS
}SEG;
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	RI i; for (scanf("%d",&n),i=1;i<n;++i) scanf("%d",&a[i]);
	for (scanf("%d",&q),SEG.build(),i=1;i<=q;++i)
	{
		scanf("%d%d",&k,&x); a[k]=x; if (k>1) SEG.updata(k);
		Matrix A(1,2); A[0][1]=a[1]; A=A*SEG.query();
		printf("%lld\n",2LL*(n!=2?A[0][1]:a[1]));
	}
	return 0; 
}

F. Communication Towers

我已经属于变成那种连线段树分治这种东西的名字都想不起来的老年菜鸡了,只能看看别人的题解了

首先我们发现如果对于接收频率进行线段树分治,那么每条边只会出现在\(O(n\log n)\)个线段树节点上

一个naive的想法就是用可撤销并查集维护到\(1\)的联通块,但是这样在叶子节点统计答案的时候可能会退化成\(O(n^2)\)

不过有一种好方法就是先在叶子节点打一个标记,然后在栈中撤销的时候把这个标记传回去即可

但是转念一想又有一个问题,如果我们只是简单地用bool类型的数组来维护标记,那么在涉及到不同的叶子节点带来的影响时可能会搞混掉

而且这个题也没法简单地用时间戳来解决这个问题,因为我们不能进行和赋值有关的操作,不然在可能会用一个叶子的信息覆盖掉另一个叶子的信息

那么改怎么处理呢,有一个很trick性的做法就是打差分标记,具体实现可以看代码,我只能说是惊为天人

那么这题就做完了,用按秩合并维护带撤销并查集,总复杂度\(O(n\log^2 n)\)

#include<cstdio>
#include<iostream>
#include<vector>
#include<utility>
#define RI register int
#define CI const int&
using namespace std;
typedef pair <int,int> pi;
const int N=200005;
struct Data
{
	int x,y,szy;
	inline Data(CI X=0,CI Y=0,CI SZY=0)
	{
		x=X; y=Y; szy=SZY;
	}
}stk[N*20]; int n,m,l[N],r[N],x,y,top,fa[N],sz[N],tag[N];
inline int getfa(CI x)
{
	return fa[x]!=x?getfa(fa[x]):x;
}
inline void merge(int x,int y)
{
	x=getfa(x); y=getfa(y); if (x==y) return;
	if (sz[x]>sz[y]) swap(x,y); stk[++top]=Data(x,y,sz[y]);
	fa[x]=y; sz[y]+=sz[x]==sz[y]; tag[x]-=tag[y];
}
inline void revoke(CI tmp)
{
	while (top>tmp)
	{
		int x=stk[top].x,y=stk[top].y,szy=stk[top].szy;
		--top; fa[x]=x; sz[y]=szy; tag[x]+=tag[y];
	}
}
class Segment_Tree
{
	private:
		vector <pi> v[N<<2];
	public:
		#define TN CI now=1,CI l=1,CI r=200000
		#define LS now<<1,l,mid
		#define RS now<<1|1,mid+1,r
		inline void insert(CI beg,CI end,const pi& it,TN)
		{
			if (beg<=l&&r<=end) return v[now].push_back(it); int mid=l+r>>1;
			if (beg<=mid) insert(beg,end,it,LS); if (end>mid) insert(beg,end,it,RS);
		}
		inline void solve(TN)
		{
			int tmp=top; for (auto [x,y]:v[now]) merge(x,y);
			if (l==r) return ++tag[getfa(1)],revoke(tmp);
			int mid=l+r>>1; solve(LS); solve(RS); revoke(tmp);
		}
		#undef TN
		#undef LS
		#undef RS
}SEG;
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	RI i; for (scanf("%d%d",&n,&m),i=1;i<=n;++i)
	scanf("%d%d",&l[i],&r[i]),fa[i]=i,sz[i]=1;
	for (i=1;i<=m;++i)
	{
		scanf("%d%d",&x,&y); int L=max(l[x],l[y]),R=min(r[x],r[y]);
		if (L<=R) SEG.insert(L,R,make_pair(x,y));
	}
	for (SEG.solve(),i=1;i<=n;++i) if (tag[i]) printf("%d ",i);
	return 0; 
}

Postscript

发现Edu的场题目其实很不错的说,而且总能学到点新trick,下次有机会要积极打一下的说