Typesetting math: 1%

AtCoder Grand Contest 025

Preface

这场ABC极水,D的构造没想出来太弱了啊,EF都挺难的,F只是口胡了一下懒得写了


A - Digits Sum

暴力枚举AAO(logN)暴力判断即可

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
int n,ans=1e9;
inline int count(int x,int ret=0)
{
	while (x) ret+=x%10,x/=10; return ret;
}
int main()
{
	RI i; for (scanf("%d",&n),i=1;i<=n-i;++i)
	ans=min(ans,count(i)+count(n-i)); return printf("%d",ans),0;
}

B - RGB Coloring

考虑把K分解成x×A+y×B(x,y[0,n]),此时方案数显然就是Cxn×Cyn

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
#define int long long
using namespace std;
const int N=300005,mod=998244353;
int n,a,b,k,ans,fact[N],inv[N];
inline int quick_pow(int x,int p=mod-2,int mul=1)
{
	for (;p;p>>=1,x=x*x%mod) if (p&1) mul=mul*x%mod; return mul;
}
inline void init(CI n)
{
	RI i; for (fact[0]=i=1;i<=n;++i) fact[i]=fact[i-1]*i%mod;
	for (inv[n]=quick_pow(fact[n]),i=n-1;~i;--i) inv[i]=inv[i+1]*(i+1)%mod;
}
inline int C(CI n,CI m)
{
	return fact[n]*inv[m]%mod*inv[n-m]%mod;
}
signed main()
{
	RI i; for (scanf("%lld%lld%lld%lld",&n,&a,&b,&k),init(n),i=0;i<=n;++i)
	if (k>=i*a&&(k-i*a)%b==0&&(k-i*a)/b<=n) (ans+=C(n,i)*C(n,(k-i*a)/b)%mod)%=mod;
	return printf("%lld",ans),0;
}

C - Interval Game

巨大收获:move竟然也是STL函数,在algorithm库中,具体作用不详(因为这个CE了好久)

首先有个显然的结论,走路的人一定会选择当前要去的区间内离自身最近的点,换句话说对于他局部最优就是全局最优

那么我们很容易想到策略去让他多走,给所有区间分别按左端点从大到小,右端点从小到大排序,用两个指针维护一下让他左右横跳即可

注意从哪边开始需要枚举下,总复杂度O(nlogn)(因为要排序)

#include<cstdio>
#include<iostream>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
const int N=100005;
struct interval
{
	int l,r,id;
	inline interval(CI L=0,CI R=0) { l=L; r=R; }
}a[N],b[N]; int n,pos,ra[N],rb[N]; bool ca[N],cb[N]; long long ans1,ans2;
inline bool cmp1(const interval& A,const interval& B)
{
	return A.l>B.l;
}
inline bool cmp2(const interval& A,const interval& B)
{
	return A.r<B.r;
}
inline int travel(const interval& p)
{
	if (p.l<=pos&&pos<=p.r) return 0; int tp=pos;
	if (pos<p.l) return pos=p.l,p.l-tp; else return pos=p.r,tp-p.r;
}
int main()
{
	RI i,j; for (scanf("%d",&n),i=1;i<=n;++i)
	scanf("%d%d",&a[i].l,&a[i].r),a[i].id=i,b[i]=a[i];
	sort(a+1,a+n+1,cmp1); sort(b+1,b+n+1,cmp2);
	for (i=1;i<=n;++i) ra[a[i].id]=rb[b[i].id]=i;
	for (i=j=1;i<=n||j<=n;++j)
	{
		while (i<=n&&ca[i]) ++i; ans1+=travel(a[i]); ca[i]=cb[rb[a[i].id]]=1;
		while (j<=n&&cb[j]) ++j; ans1+=travel(b[j]); cb[j]=ca[ra[b[j].id]]=1;
	}
	ans1+=travel(interval(0,0));
	for (i=1;i<=n;++i) ca[i]=cb[i]=0;
	for (i=j=1;i<=n||j<=n;++j)
	{
		while (j<=n&&cb[j]) ++j; ans2+=travel(b[j]); cb[j]=ca[ra[b[j].id]]=1;
		while (i<=n&&ca[i]) ++i; ans2+=travel(a[i]); ca[i]=cb[rb[a[i].id]]=1;
	}
	ans2+=travel(interval(0,0));
	return printf("%lld",max(ans1,ans2)),0;
}

D - Choosing Points

很有趣的一题但是脑子里只想着暴力了没往(12)2=n24n2=14上想,直接GG

我们考虑对于每一个D,构造一种方案使得有一半位置能取,另一半位置都不能取

这样把D1,D2的情况合并起来之后至少会有n2个位置能取,因此现在考虑对于某个D如何构造,设D2=x2+y2(x,yN),假设在这个位置上填0表示可取

  • Dmod2=1时:

    此时显然x,y一奇一偶,那么我们只需要让所有选取的位置的两维坐标之和的奇偶性相同即可,这个很显然直接间隔填即可

    010101010

  • Dmod4=2时:

    此时显然x,y均为奇数,那么我们只需要让所有选取的位置的两维坐标中存在至少一维为偶数即可,这个很显然可以直接把整行选取然后在隔行再选

    000111000

  • Dmod4=0时:

    此时我们考虑D2=x2+y2D24=(x2)2+(y2)2,因此我们每次把D除以4然后把一块2×2的位置看做一块再递归处理即可

综上,最后把两个矩阵加起来所有为0的位置都可取

#include<cstdio>
#define RI register int
#define CI const int&
using namespace std;
const int N=305;
int n,d1,d2,cur,a[N<<1][N<<1];
inline void solve(int d)
{
	int t=1,c=1; RI i,j,p,q; while (d%4==0) d/=4,t*=2;
	if (d%2==1)
	{
		for (i=0;i<2*n;++i) for (j=0;j<2*n;++j) a[i][j]+=(((i/t)^(j/t))&1);
	} else
	{
		for (i=0;i<2*n;++i) for (j=0;j<2*n;++j) a[i][j]+=((i/t)&1);
	}
}
int main()
{
	RI i,j; scanf("%d%d%d",&n,&d1,&d2); solve(d1); solve(d2);
	for (i=0;i<2*n;++i) for (j=0;j<2*n;++j) if (!a[i][j]&&cur<n*n)
	printf("%d %d\n",i,j),++cur; return 0;
}

E - Walking on a Tree

把最优化问题变成构造问题可真有你的

首先我们发现设一条边i被经过的次数为ci,那么答案的上界显然是n1i=1min(ci,2)

接下来我们考虑一种构造方案让它卡到上界,设(x,y)表示x,y之间的树边[x,y]表示x,y之间的路径(不考虑方向)

首先我们任取一个叶子节点x,设与之相连的点为y

  • c(x,y)=0直接把x(x,y)删了对答案也没影响
  • c(x,y)=1,假设存在一条经过(x,y)的路径为[x,a],则[x,a](x,y)造成的贡献与[x,a]的方向无关,显然可以直接把[x,a]改成[y,a]再把x(x,y)删了
  • c(x,y)2,任取两条经过(x,y)的路径[x,a],[x,b],为了使(x,y)的贡献达到上界那么我们强制[x,a][x,b]方向不同。设[x,a],[x,b]分叉点z,则[x,z]上的所有边贡献都是2,分叉后的路径可以等价于一条路径[a,b](其中[a,b]走向会制约[x,a].[x,b]的走向)。然后对于剩下的路径按c(x,y)=1的方式传递给y,再把x(x,y)删掉即可

不难发现我们重复执行这个过程到仅剩一个点时,上述方法构造的方案满足每条树边的贡献都卡到了上界,因此一定是最优的

考虑如何实现,首先我们可以用set维护以每个点为端点的路径,同时对于每条路径我们记录一下它当前的端点以及目前的方向

最后注意到路径分叉时新路径[a,b]对原路径方向的制约,我们类似并查集一样记录一下制约点最后扫一遍

复杂度O(n2logn)(设n,m同阶)

#include<cstdio>
#include<set>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=2005;
struct edge
{
	int to,nxt;
}e[N<<1]; int n,m,head[N],cnt=1,x,y,deg[N],q[N],c[N],ans;
struct element
{
	int rx,ry,nx,ny,rev,fa;
	inline void fix(CI x,CI y)
	{
		if (nx==x) nx=y; else ny=y;
	}
}d[N]; set <int> s[N]; bool vis[N];
inline void addedge(CI x,CI y)
{
	e[++cnt]=(edge){y,head[x]}; head[x]=cnt; ++deg[x];
	e[++cnt]=(edge){x,head[y]}; head[y]=cnt; ++deg[y];
}
#define to e[i].to
inline void BFS(void)
{
	RI H=0,T=0,i; int a,b; for (i=1;i<=n;++i) if (deg[i]==1) q[++T]=i;
	while (H<T)
	{
		int x=q[++H],y; for (a=0,i=head[x];i;i=e[i].nxt)
		if (deg[y=to]>1) { a=i>>1; break; }
		if (--deg[y]==1) q[++T]=y; if (s[x].empty()) continue;
		if (s[x].size()==1)
		{
			while (!s[x].empty())
			{
				a=*s[x].begin(); s[x].erase(s[x].begin()); d[a].fix(x,y);
				if (d[a].nx!=d[a].ny) s[y].insert(a); else s[d[a].nx].erase(a);
			}
		} else
		{
			a=*s[x].begin(); s[x].erase(s[x].begin());
			b=*s[x].begin(); s[x].erase(s[x].begin());
			if (d[a].ny==x) swap(d[a].nx,d[a].ny),d[a].rev^=1;
			if (d[b].ny==x) swap(d[b].nx,d[b].ny),d[b].rev^=1;
			s[d[a].ny].erase(a); s[d[b].ny].erase(b); d[a].nx=d[b].ny;
			if (d[a].nx!=d[a].ny) s[d[a].nx].insert(a),s[d[a].ny].insert(a);
			d[b].fa=a; d[b].rev^=d[a].rev^1; while (!s[x].empty())
			{
				a=*s[x].begin(); s[x].erase(s[x].begin()); d[a].fix(x,y);
				if (d[a].nx!=d[a].ny) s[y].insert(a); else s[d[a].nx].erase(a);
			}
		}
	}
}
inline int travel(CI now,CI tar,CI fa=0)
{
	if (now==tar) return 1; for (RI i=head[now];i;i=e[i].nxt)
	if (to!=fa&&travel(to,tar,now)) return ++c[i>>1],1; return 0;
}
#undef to
inline int getrev(CI now)
{
	if (!now) return 0; if (vis[now]) return d[now].rev;
	vis[now]=1; return d[now].rev^=getrev(d[now].fa);
}
int main()
{
	RI i; for (scanf("%d%d",&n,&m),i=1;i<n;++i)
	scanf("%d%d",&x,&y),addedge(x,y); for (i=1;i<=m;++i)
	scanf("%d%d",&d[i].rx,&d[i].ry),d[i].nx=d[i].rx,d[i].ny=d[i].ry,
	s[d[i].rx].insert(i),s[d[i].ry].insert(i);
	for (BFS(),i=1;i<=m;++i) travel(d[i].rx,d[i].ry);
	for (i=1;i<n;++i) ans+=min(c[i],2); printf("%d\n",ans);
	for (i=1;i<=m;++i)
	{
		if (getrev(i)) swap(d[i].rx,d[i].ry); printf("%d %d\n",d[i].rx,d[i].ry);
	}
	return 0;
}

F - Addition and Andition

大致看懂了题解口胡了下做法,但是实现太烦了就懒得写了

首先考虑朴素的暴力,进行k次加法,从低位到高位,如果两个串当前位上是(1,1),那么可以把这两位清空然后往前进位

然后我们容易发现这个过程可以从高位到低位处理,这样可以避免掉连续的(1,1)带来的连续进位的麻烦,同时也让高位与低位独立开来可以分别处理

然后我们很容易想到改变一下枚举的顺序,从高位到低位枚举每一位,如果两个串当前位上是(1,1),那么就往前做k次“进位操作”,其中“进位操作”具体来说就是:

  • 将当前位置的(1,1)清空为(0,0)
  • 考虑当前位置的前一个位置:
    • 若为(0,0),把该为改为(1,1)然后把指针往前移动一位
    • 若为(1,0),把第二个串对应位置清空并且给第一个串进位
    • 若为(0,1),把第一个串对应位置清空并且给第二个串进位
    • 若为(1,1),从高位往低位做显然不会出现这样的问题

若是存在形如(100,011)变成(101,100)的情况,那么可以直接把指针移到前面的(1,1)

用链表来维护状态相同的连续段,模拟即可,考虑对时间复杂度势能分析

  • 当前面是连续的(0,0)时,可以O(1)跳过去
  • 当前面时连续的(1,0)时,可能会越过这段连续的(1,0)并在前面创造新的(1,1),但是这个过程必然伴随着这一段连续的(1,0)变为(0,0)(除了最后一个变成了(1,0)
  • 当前面时连续的(0,1)时,和上面类似

因此当我们把(1,0)连着(0,1)的位置的个数定义为势能的话,时间复杂度就是O(n)


Postscript

之前一直能Rush出来的构造题没做出来有点可惜啊233

posted @   空気力学の詩  阅读(144)  评论(0编辑  收藏  举报
编辑推荐:
· Java 中堆内存和栈内存上的数据分布和特点
· 开发中对象命名的一点思考
· .NET Core内存结构体系(Windows环境)底层原理浅谈
· C# 深度学习:对抗生成网络(GAN)训练头像生成模型
· .NET 适配 HarmonyOS 进展
阅读排行:
· 用 DeepSeek 给对象做个网站,她一定感动坏了
· DeepSeek+PageAssist实现本地大模型联网
· 手把手教你更优雅的享受 DeepSeek
· 腾讯元宝接入 DeepSeek R1 模型,支持深度思考 + 联网搜索,好用不卡机!
· 从 14 秒到 1 秒:MySQL DDL 性能优化实战
历史上的今天:
2018-10-27 Luogu P1447 [NOI2010]能量采集
点击右上角即可分享
微信分享提示