ARC142

ARC142

考试情况:一眼订正,鉴定为做出前三题。

A - Reverse and Minimize

分析题目性质可得三种情况:

  • \(K\) 末尾有 \(0\)

    最多只有 \(K\) 本身一个答案。

  • \(K\) 是回文数

    去掉翻转的情况。

  • 其它

    考虑 \(K\) 翻转之后的数比它大还是小。

    • 翻转过后的数比 \(K\)

      \(K\) 以及它的不超过 \(N\)\(10\) 的整数倍和其翻转后的数及不超过 \(N\)\(10\) 的整数倍都是答案。

    • 否则

      答案为 \(0\)

我因为考试的时候没看到Find the minimum possible value of x after operations.这句话,所以没有判断得到最后的那种情况,所以卡了很久。

代码:

点击查看代码
#include <bits/stdc++.h>
using namespace std;
int ch[200],Len=0;
bool HuiWen(long long a)
{
	while(a)
	{
		ch[++Len]=a%10;
		a/=10;
	}
	for(int i=1,j=Len;i<j;i++,j--)
	{
		if(ch[i]!=ch[j])
		return false;
	}
	return true;
}
int main()
{
	long long N,K;
	int ans=0;
	cin>>N>>K;
	if(HuiWen(K))
	{
		while(K<=N)
		{
			ans++;
			K*=10;
		}
		cout<<ans;
		return 0;
	}
	if(K%10==0)
	{
		if(N>=K)
		cout<<1;
		else
		cout<<0;
		return 0;
	}
	long long K1=0,K2=K;
	while(K)
	{
		K1=K1*10+K%10;
		K/=10;
	}
	if(K1<=K2)
	{
		cout<<0;
		return 0;
	}
	while(K1<=N)
	{
		ans++;
		K1=K1*10;
	}
	while(K2<=N)
	{
		ans++;
		K2=K2*10;
	}
	cout<<ans;
}

B - Unbalanced Squares

构造题,没有任何思路。但我会暴力。

所以就随便构造一个,然后只要一判断到不合法就把那个位置跟其它位置交换。直到没有不合法位置为止。

竟然过了。

代码

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN=505;
int a[MAXN][MAXN];
int N;
int dx[9]={0,-1,-1,-1,0,0,1,1,1},dy[9]={0,-1,0,1,-1,1,-1,0,1};
bool Can(int x,int y)
{
	if(x<1||x>N||y<1||y>N)
	return false;
	return true;
}
int main()
{
	cin>>N;
	for(int i=1;i<=N;i++)
	{
		for(int j=1;j<=N;j++)
		{
			a[i][j]=(i-1)*N+j;
		}
	}	
	while(true)
	{
		bool flag=false;
		for(int i=2;i<=N-1;i++)
		{
			for(int j=2;j<=N-1;j++)
			{
				int Cnt=0,fx1=0,fy1=0;
				for(int op=1;op<=8;op++)
				{
					int fx=i+dx[op],fy=j+dy[op];
					if(Can(fx,fy))
					{
						if(a[fx][fy]>a[i][j])
						{
							Cnt++;
							if(fx1==0)
							{
								fx1=fx;
								fy1=fy;
							}
						}
					}
				}
				if(Cnt==4)
				{
					swap(a[i][j],a[fx1][fy1]);
					flag=true;
				}
			}
		 } 
		 if(flag==false)
		 break;
	}
	for(int i=1;i<=N;i++)
	{
		for(int j=1;j<=N;j++)
		{
			printf("%d ",a[i][j]);
		}
		puts("");
	}
}

C - Tree Queries

Atcoder 罕见的交互题。

首先求出 \(1\)\(2\) 到其它所有点的距离,需要不到 \(2N\) 次操作。

找出距离 \(1\)\(2\) 距离为 \(1\) 的点(如果没有就直接输出 \(1\) 就完事了)。这些点与 \(1\)\(2\) 的关系是确定的。

找出与 \(1\) 距离最近的 \(2\) 的相邻点,找出与 \(2\) 距离最近的 \(1\) 的相邻点。其实就相当于找出 \(1\)\(2\) 中间那条链。但是注意到可能并不会找到这条链,此时 \(1\)\(2\) 的距离为 \(1\)\(1\) 找到的点距离为 \(2\)\(2\) 找到的点距离也为 \(2\)。同样,也存在 \(1\)\(2\) 距离为 \(3\) 时,\(1\) 找到的点距离为 \(2\)\(2\) 找到的点距离也为 \(2\)。所以当遇到这种情况时,再问一次 \(1\) 找到的点与 \(2\) 找到的点的距离,如果为 \(1\) 则答案为 \(3\) 否则答案为 \(1\)

其它情况答案为最近相邻点距离加 \(1\)

代码

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN=505;
int N;
int dis[MAXN][MAXN];
vector<int>Near1,Near2;
int main()
{
	cin>>N;
	for(int i=3;i<=N;i++)
	{
		cout<<"? 1 "<<i<<'\n';
		cout.flush();
		cin>>dis[1][i];
		if(dis[1][i]==1)
		{
			Near1.push_back(i);
		}
	}
	for(int i=3;i<=N;i++)
	{
		cout<<"? 2 "<<i<<'\n';
		cout.flush();
		cin>>dis[2][i];
		if(dis[2][i]==1)
		{
			Near2.push_back(i);
		}
	}
	if(Near1.size()==0||Near2.size()==0)
	{
		cout<<"! 1";
		return 0;
	}
	int Min1id=0,Min2id=0;
	for(int i=0;i<Near1.size();i++)
	{
		if(Min1id==0||dis[2][Min1id]>dis[2][Near1[i]])
		Min1id=Near1[i];
	}
	int Cnt=0;
	for(int i=0;i<Near1.size();i++)
	{
		if(dis[2][Min1id]==dis[2][Near1[i]])
		Cnt++;
	}
	if(Cnt>1)
	{
		cout<<"! 1";
		return 0;
	}
	for(int i=0;i<Near2.size();i++)
	{
		if(Min2id==0||dis[1][Min2id]>dis[1][Near2[i]])
		Min2id=Near2[i];
	}
	Cnt=0;
	for(int i=0;i<Near2.size();i++)
	{
		if(dis[1][Min2id]==dis[1][Near2[i]])
		Cnt++;
	}
	if(Cnt>1)
	{
		cout<<"! 1";
		return 0;
	}
	int Min1=dis[2][Min1id],Min2=dis[1][Min2id];
	if(Min1==2)
	{
		cout<<"? "<<Min1id<<" "<<Min2id<<'\n';
		cout.flush();
		int x;
		cin>>x;
		if(x==3)
		cout<<"! "<<1;
		else
		cout<<"! "<<Min1+1;
		return 0;
	}
	cout<<"! "<<Min1+1;
	return 0;
}

D - Deterministic Placing

将 piece 叫做石头吧。

题目的一个特殊要求就是要每一步每一颗石子都有且只有一条路可以走。看完样例一就会明白的。

定义滑动路径(sliding path)表示树上一条经过边数至少为 \(1\) 的简单路径,其中有且只有 \(1\) 个点为空节点,其它节点全都有石头。把这棵树分成若干条不相交的滑动路径,在每条滑动路径上,每次操作就是让所有点都朝着空节点移动一条边,轻松满足无限次操作。然后再考虑每一步每一颗石子都有且只有一条路可以走。

然后借鉴这篇博客的思路,定义每条滑动路径没有石子的端点为 \(0\) 端,有棋子的端点为 \(1\) 端,中间的点称为中间点。

结论是只要划分的树上存在不同滑动路径上的两个点 \(x\)\(y\) 满足【\(x\) 为中间点 \(y\) 为端点】或【\(x\)\(y\) 都为 \(1\) 端】或【\(x\)\(y\) 都为 \(0\) 端】则此划分不合法。因为我确实脑袋不太灵活,所以就不放证明了,证明可以去看上文提到的博客。

所以所有路径都是 \(0\) 端对 \(1\) 端的,只要有分叉必定是要分路径的。

然后 dp 出合法的划分数量。分 \(5\) 类讨论。

分类讨论有点恶心。所以最后才补。

对于点 \(u\),只考虑其滑动路径上且在其子树内的点。

类型一:\(0\) 端在其子树内或为其本身,\(1\) 端不在其子树内且不为其本身。(根为 \(1\) 端)

类型二:\(1\) 端在其子树内或为其本身,\(0\) 端不在其子树内且不为其本身。(根为 \(0\) 端)

类型三:自己就是 \(1\) 端,且子树内存在 \(0\) 端。(根为 \(1\) 端)

类型四:自己就是 \(0\) 端,且子树内存在 \(1\) 端。(根为 \(0\) 端)

类型五:\(0\) 端和 \(1\) 端都在其子树内且不为其本身,自己是这条滑动路径上的根。(根为自己)

对于类型一:

如果自己就是 \(0\) 端,则 \(f_{u,1}=f_{v,3}\)

如果自己是中间点,则 \(f_{u,1}=\sum_{v} f_{v,1} \prod_{other v}f_{otherv,5}\)

对于类型二:

如果自己是 \(1\) 端,则 \(f_{u,2}=f_{v,4}\)

否则:\(f_{u,2}=\sum_{v}f_{v,2}\prod_{otherv}f_{otherv,5}\)

对于类型三:

一个儿子贡献 \(f_{v,2}\),其它儿子贡献 \(f_{v,4}\)

对于类型四:

一个儿子贡献 \(f_{v,1}\),其它儿子贡献 \(f_{v,3}\)

对于类型五:

一个儿子贡献一个\(f_{v,1}\),另一个儿子贡献一个 \(f_{v,2}\),剩下的儿子贡献 \(f_{v,5}\)

根节点只能是类型三、四、五。

代码:

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN=1e6+50,P=998244353;
struct Edge
{
	int x,y,Next;
}e[MAXN<<1];
int elast[MAXN],tot;
void Add(int x,int y)
{
	tot++;
	e[tot].x=x;
	e[tot].y=y;
	e[tot].Next=elast[x];
	elast[x]=tot;
}
int N;
long long f[MAXN][6],g[6];
void dfs(int u,int fa)
{
	for(int i=elast[u];i;i=e[i].Next)
	{
		int v=e[i].y;
		if(v==fa)
		continue;
		dfs(v,u);
	}
	//Pattern 1
	long long t=1;
	g[1]=1;
	g[2]=0;
	for(int i=elast[u];i;i=e[i].Next)
	{
		int v=e[i].y;
		if(v==fa)
		continue;
		t=t*f[v][4]%P;
		g[2]=(g[2]*f[v][5]+g[1]*f[v][1])%P;
		g[1]=g[1]*f[v][5]%P;
	}
	f[u][1]=(f[u][1]+t+g[2])%P;
	//Pattern 2
	g[1]=1;
	g[2]=0;
	t=1;
	for(int i=elast[u];i;i=e[i].Next)
	{
		int v=e[i].y;
		if(v==fa)
		continue;
		t=t*f[v][3]%P;
		g[2]=(g[2]*f[v][5]+g[1]*f[v][2])%P;
		g[1]=g[1]*f[v][5]%P;
	}
	f[u][2]=(f[u][2]+t+g[2])%P;
	//Pattern 3
	g[1]=1;
	g[2]=0;
	for(int i=elast[u];i;i=e[i].Next)
	{
		int v=e[i].y;
		if(v==fa)
		continue;
		g[2]=(g[2]*f[v][4]+g[1]*f[v][2])%P;
		g[1]=(g[1]*f[v][4])%P;
	}
	f[u][3]=(f[u][3]+g[2])%P;
	//Pattern 4
	g[1]=1;
	g[2]=0;
	for(int i=elast[u];i;i=e[i].Next)
	{
		int v=e[i].y;
		if(v==fa)
		continue;
		g[2]=(g[2]*f[v][3]+g[1]*f[v][1])%P;
		g[1]=(g[1]*f[v][3])%P;
	}
	f[u][4]=(f[u][4]+g[2])%P;
	//Pattern 5
	g[1]=1;
	g[2]=g[3]=g[4]=0;
	for(int i=elast[u];i;i=e[i].Next)
	{
		int v=e[i].y;
		if(v==fa)
		continue;
		long long fv=f[v][5];
		g[4]=(g[4]*fv+g[3]*f[v][1]+g[2]*f[v][2])%P;
		g[3]=(g[3]*fv+g[1]*f[v][2])%P;
		g[2]=(g[2]*fv+g[1]*f[v][1])%P;
		g[1]=(g[1]*fv)%P;
	}
	f[u][5]=(f[u][5]+g[4])%P;
}
int main()
{
	scanf("%d",&N);
	for(int i=1;i<N;i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		Add(x,y);
		Add(y,x);
	}
	dfs(1,0);
	printf("%lld",(f[1][3]+f[1][4]+f[1][5])%P);
    return 0;
}

E - Pairing Wizards

Part 1

对于每一对点 \((i,j)\)\(a_i=a_j=\min(b_i,b_j)\)。去掉已经满足条件的点对。剩下的点对一定是二分图。

根据题解的说法,在执行以上操作后,对于所有 \(a_i<b_i\) 的点,都在集合 \(X\) 里,\(a_i \geq b_i\) 的点,都在集合 \(Y\) 里,剩下的点对一定是一个点在 \(X\) 里一个点在 \(Y\) 里。所以是二分图森林。

Part 2

需要的点:

源点 \(S\),汇点 \(T\)

对于每个在 \(X\) 集合里的点都有 \(1\) 个对应点。

对于每个在 \(Y\) 集合里的点都有 \(100\) 个对应点(代表权值增量从 \(1\)\(100\))。

Part 3

需要的边:

源点连向每一个 \(X\) 集合的点的对应点,权值为 \(b_i-a_i\)

每个 \(Y\) 集合的对应点都连向 \(T\),权值为 \(1\)

对于每个 \(Y\) 集合里的点,所对应的 \(100\) 个点,从大往小连成一条链,权值都为 \(\inf\)

每一对 \((x,y)\)\(x\) 的对应点连向 \(y\)\(b_x-a_y\) 号对应点,权值为 \(\inf\)

跑最小割即可。

代码:

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN=1e6+50,INF=1e8;
struct Edge 
{
	int x,y;
	int G;
	int Next;
} e[MAXN<<2];
int elast[MAXN];
int Cnt[MAXN],Dis[MAXN];
int N,M,flow,ans;
int Tot=1;
bool Inq[MAXN];
int Cur[MAXN];
void Add(int x,int y,int G) 
{
	Tot++;
	e[Tot].x=x;
	e[Tot].y=y;
	e[Tot].G=G;
	e[Tot].Next=elast[x];
	elast[x]=Tot;
}
int s,t;
int dfs(int u,int In) 
{
	if(u==t) 
	{
		return In;
	}
	int flow,Out=0;
	for(int i=Cur[u];i>1;i=e[i].Next) 
	{
		Cur[u]=i;
		int v=e[i].y;
		if(e[i].G>0&&Dis[u]==Dis[v]+1) 
		{
			flow=dfs(v,min(In,e[i].G));
			In-=flow;
			Out+=flow;
			e[i].G-=flow;
			e[i^1].G+=flow;
			if(In<=0||Dis[s]>=t) 
			{
				return Out;
			}
		}
	}
	if(Dis[s]>=t) 
	{
		return Out;
	}
	Cnt[Dis[u]]--;
	if(Cnt[Dis[u]]==0)
		Dis[s]=t;
	Dis[u]++;
	Cnt[Dis[u]]++;
	Cur[u]=elast[u];
	return Out;
}
bool Flag;
void bfs(int u)
{
	queue<pair<int,int> >q;
	q.push(make_pair(u,0)); 
	Inq[u]=true;
	while(!q.empty())
	{
		int tt=q.front().first,step=q.front().second;
		q.pop();
		Dis[tt]=step;
		for(int j=elast[tt];j>1;j=e[j].Next)
		{
			int v=e[j].y;
			if(Inq[v]==false&&e[j].G>0)
			{
				q.push(make_pair(v,step+1));
				Inq[v]=true;
			}
		}
	}
	for(int j=0;j<=t;j++)
		Cnt[Dis[j]]++;	
}
int a[MAXN],b[MAXN];
int x[MAXN],y[MAXN];
vector<int>X,Y,E;
int InX[MAXN],InY[MAXN];
int main() 
{
	cin>>N;
	s=0;
	t=101*(N+1)+1;
	for(int i=1;i<=N;i++)
	{
		cin>>a[i]>>b[i];
	}
	cin>>M;
	for(int i=1;i<=M;i++)
	{
		cin>>x[i]>>y[i];
		int Minb=min(b[x[i]],b[y[i]]);
		ans+=max(0,Minb-a[x[i]]);
		ans+=max(0,Minb-a[y[i]]);
		a[x[i]]=max(a[x[i]],Minb);
		a[y[i]]=max(a[y[i]],Minb);
	}
	int totDot=0;
	for(int i=1;i<=M;i++)
	{
		if(a[x[i]]>=b[x[i]])
		swap(x[i],y[i]);
		if(a[y[i]]<b[x[i]])
		{
			if(InX[x[i]]==0)
			{
				X.push_back(x[i]);
				InX[x[i]]=X.size();
			}
			if(InY[y[i]]==0)
			{
				Y.push_back(y[i]);
				InY[y[i]]=Y.size();
			}
			E.push_back(i);
		}
	}
	for(int i=1;i<=X.size();i++)
	{
		int fx=X[i-1];
		Add(s,fx*101,max(0,b[fx]-a[fx]));
		Add(fx*101,s,0);
	}
	for(int i=1;i<=Y.size();i++)
	{
		int fx=Y[i-1];
		for(int j=fx*101+100;j>=fx*101+1;j--)
		{
			Add(j,j-1,INF);
			Add(j-1,j,0);
			Add(j,t,1);
			Add(t,j,0);
		}
		Add(fx*101,t,0);
		Add(t,fx*101,0);
	}
	for(int i=1;i<=E.size();i++)
	{
		int fx=x[E[i-1]],fy=y[E[i-1]];
		Add(fx*101,fy*101+b[fx]-a[fy],INF);
		Add(fy*101+b[fx]-a[fy],fx*101,0);
	}
	bfs(t);
	while(Dis[s]<t)
	{
		flow=dfs(s,INF);
		ans+=flow;
	}
	cout<<ans;
}

F - Paired Wizards

\(a_i=c_i\) 称之为 \(X\) 固定(fixed),将 \(b_i=d_i\) 称之为 \(Y\) 固定。

分类讨论:

类型一(Pattern 1):都未固定且 \(a_i \neq b_i\)。即:总有一个人蓄力,另一个人攻击。

类型二:都未固定且 \(a_i=b_i\)。即:要不然都蓄力,要不然都攻击,无法确定是否攻击。

类型三:\(X\) 固定 \(Y\) 不固定。即只需要讨论 \(Y\)

类型四:\(Y\) 固定 \(X\) 不固定。即只需要讨论 \(X\)

类型五:\(X\) 固定 \(Y\) 固定。

其中类型五没有任何意义,因为没得选所以只需要讨论前四种类型。

\(X\) 用了 \(C_X\) 次法术 \(2\),按照使用时间从小到大为 \(x_1,x_2...x_{C_X}\)。其中第 \(i\) 次可以得到的伤害为 \(x_i-i\),即当前时间为 \(x_i\),其中有 \(i\) 次用了法术 \(2\),则用了 \(x_i-i\) 次法术 \(1\),即蓄力到攻击力为 \(x_i-i\)

所以可以得到 \(X\) 的总伤害为 \(\sum_{i=1}^{C_X}(x_i-i)=\sum_{i=1}^{C_X}x_i-\sum_{i=1}^{C_X}i\)

若设 \(S_X=\sum_{i=1}^{C_X}x_i\),又因为后式可以可以公式计算,则 \(X\) 的总伤害为 \(S_X-\frac{C_X(C_X-1)}{2}\)

\(Y\) 同理,则答案为 \(\max(S_X+S_Y-\frac{C_X(C_X-1)}{2}-\frac{C_Y(C_Y-1)}{2})\)

分开计算就有个好处,就是 \(X\)\(Y\) 差别不大了。

对于类型一,\(S_X+S_Y\) 是不变的。但是 \(C_X\)\(C_Y\) 会有差别,考虑枚举在所有类型一里,\(C_X\) 有多少,这样就确定完类型一了。因为答案反正跟中途过程无关,所以不需要管哪一步是谁选。

对于类型二,贪心地看,后面的操作全都施法,前面的操作全都蓄力显然是最优的,可以用邻项交换法证明。所以只需要枚举要施几次法(也可以枚举蓄几次力),然后类型二就确定了。

对于类型三和四,同类型二,先蓄力后施法,只需要枚举施几次法(或蓄几次力)即可。

时间复杂度 \(O(N^4)\)

秋豆麻袋!过不去。

于是题解告诉我,在确定类型一和类型二中的选择后,类型三和类型四中的最佳选择可以分别确定,并且它们只取决于 \(C_X\)\(C_Y\) 的值。所以预处理 \(f_{i,0}\) 表示当 \(C_X=i\) 时类型三的最佳选择,\(f_{i,1}\) 表示当 \(C_Y=i\) 时类型四的最佳选择,然后枚举 \(C_X\)\(C_Y\) 即可。我代码里的Get函数即为求 \(f\) 的函数。事实证明,记忆化比预处理快上千倍。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN=1e4+50;
int N;
int Sum(int x)
{
	return x*(x-1)/2;
}
int a[MAXN],b[MAXN],c[MAXN],d[MAXN];
int f[MAXN][2];
vector<int>e[2],Vec[2];
int Cntx,Cnty;
int Get(int id,int op)
{
	if(f[id][op])
	return f[id][op];
	f[id][op]-=Sum(id);
	int NowSum=0;
	for(int i=0,j=id+1;i<e[op].size();i++,j++)
	{
		NowSum+=N-e[op][i];
		f[id][op]=max(f[id][op],NowSum-Sum(j));
	}
	return f[id][op];
}
int main()
{
	scanf("%d",&N);
	for(int i=1;i<=N;i++)
	{
		scanf("%d%d%d%d",&a[i],&b[i],&c[i],&d[i]);
	}
	int ans=0,Max=-1e9;
	for(int i=1;i<=N;i++)
	{
		if(a[i]==c[i])//Pattern 3
		{
			if(a[i]==1)
			{
				ans+=N-i;
				Cntx++;
			}
			if(b[i]!=d[i])
			{
				e[1].push_back(i);
			}
		}
		if(b[i]==d[i])
		{
			if(b[i]==1)
			{
				ans+=N-i;
				Cnty++;
			}
			if(a[i]!=c[i])//Pattern 4
			{
				e[0].push_back(i);
			}
		}
		if(a[i]!=b[i]&&a[i]!=c[i]&&b[i]!=d[i])//Pattern 1
		{
			ans+=N-i;
			Vec[0].push_back(i);
		}
		if(a[i]==b[i]&&a[i]!=c[i]&&b[i]!=d[i])//Pattern 2
		{
			Vec[1].push_back(i);
		}
	}
	for(int i=0;i<=Vec[0].size();i++)//Cx 
	{
		int NowSum=0;
		for(int j=0;j<Vec[1].size();j++)//Cy
		{
			Max=max(Max,NowSum+Get(Cntx+i+j,0)+Get(Cnty+Vec[0].size()-i+j,1));
			NowSum+=2*(N-Vec[1][j]);
		}
		Max=max(Max,NowSum+Get(Cntx+i+Vec[1].size(),0)+Get(Cnty+Vec[0].size()-i+Vec[1].size(),1));
	}
	printf("%d",ans+Max);
}
posted @ 2022-06-21 11:54  0htoAi  阅读(120)  评论(1编辑  收藏  举报