The 2022 ICPC Asia Regionals Online Contest (II)ABEFJ

nannandbk / 2023-08-22 / 原文

The 2022 ICPC Asia Regionals Online Contest (II)

A Yet Another Remainder

题意:给你一个正整数\(x\),但是这个数被隐藏起来了。你问了电脑\(min(100,n)\)个问题,第\(i\)轮,的第\(j\)个问题:\(O(j,i\times \lfloor\dfrac{n-j}{i}\rfloor+j,i)\),让\(l = j,r = i\times \lfloor \dfrac{n-j}{i}\rfloor + j\),你将得到\(a_l+a_{l+i}+a_{l+2_i}+...+a_r\)的值。之后有\(q\)个询问,给你一个质数\(p = 3,7\le p\le 97\),你回答这个隐藏的整数\(x \mod p\)的值是多少?

思路:首先,看问题描述我们发现几个有趣的事情:

  1. 询问的\(p\)\(100\)以内的质数,但唯独不包含\(2\)\(5\),是为什么?
  2. 题目中告诉你的都是每种步长的和,为什么要告诉我这个?和它的和有什么关系?

对于第一个事情,不包含\(2\)\(5\),说明询问的p是和\(10\)是互质的。

对于第二个事情,根据第一个想法,我们考虑,把一个数可以拆成以下形式:\(\sum_{i = 1}^{n} a[i]*10^{n-i}\)

举个例子:比如数\(998244353\),可以拆成:\(9*10^8+9*10^7+8*10^6+2*10^5+...+3*10^0\)

然后我们又发现,根据费马小定理知:如果\(a,p\)互质有:\(a^{p-1}≡1(\mod p)\),所以每\(p-1\)是一个循环节。

比如以模数为\(7\)为例,因为\(10\)\(7\)互质,所以有\(a^{p-1}≡1(\mod p)\),即\(10^{p-1}≡1(\mod p)\),设\(p-1 = k\),那么\(10^{tk} ≡ 1(\mod p)\)。也就是\(p-1\)是一个循环节,这里的话就是\(7-1 = 6\)

\(10^0,10^6,10^{12}...\)\(\mod 7\)的意义下都是等于\(1\)的,而\(10^1,10^7,10^{13}...\)\(\mod 7\)的意义下都等于\(3\)....以此类推。

\((a[1]*10^0 + a[7]*10^6+a[13]*10^{12}+...)\mod 7 = 1*(a[1]+a[7]+a[13]+...)\)

\((a[2]*10^1 + a[8]*10^7+a[14]*10^{13}+...) \mod 7 = 3*(a[2]+a[8]+a[14]+...)\)

到这里,我们发现了,题目告诉我们每个步长个数的加和是为什么了。那么下面就是做法:对于模数\(p\),我们的答案在\(p-1\)行获取(因为步长是\(p-1\),我们想要知道每隔\(p-1\)个的加和)。\(p-1\)行的第\(i\)列的加和就是\(10^i \mod p\)相等的值前面的系数了。用\(c[i][j]\)表示:\(10^j \mod i\)的值是多少(预处理),用\(b[i][j]\)表示:步长为\(i\)的第\(j\)组数的和(题目输入)。对于模数是\(p\)的答案就是:\(\sum_{i = 1}^{p-1}b[p-1][i]*c[p][(n-i)\bmod (p-1)]\)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod = 1e9 + 7;
const int N = 110;
ll c[N][N],b[N][N];
void init()
{
	for(int i = 1;i<=100;i++)
	{	
		c[i][0] = 1;
		for(int j = 1;j<=i;j++)
		{	
			c[i][j] = c[i][j-1]*10%i;
		}
	}
}

int main()
{
    ios::sync_with_stdio(false);   cin.tie(nullptr), cout.tie(nullptr);
  
    int t;
    cin>>t;
    init();
    while(t--)
    {
    	int n;
    	cin>>n;
    	for(int i = 1;i <= min(n,100);i++)
    	{
    		for(int j = 1;j<=i;j++)
    		{
    			cin>>b[i][j];
    		}
    	}

    	if(n<=100)
    	{	
    		
    		int q;
    		cin>>q;
    		while(q--)
    		{
    			int p;
    			cin>>p;
    			ll ans = 0;
	    		for(int i = 1;i<=n;i++)
	    		{
	    			ans *= 10;
	    			ans%=p;
	    			ans += b[n][i];
	    			ans %= p;
	    		}
    			cout<<ans%p<<"\n";
    		}
    	}
    	else{
    		int q;
	    	cin>>q;
	    	while(q--)
	    	{
	    		int p;
	    		cin>>p;
	    		ll ans = 0;
	    		for(int i = 1;i<=p-1;i++)
	    		{
	    			ans += b[p-1][i]%p*c[p][(n-i)%(p-1)];
	    			ans%=p;
	    		}
	    		cout<<ans<<"\n";
	    	}
    	}
    }

    return 0;
}

B Non-decreasing Array

题意:给你一个不降序列\(a_1,a_2,...,a_n\),在一次操作:第一步你可以选择一个数删了或者什么也不做。第二步你可以选择一个数把它改成任意整数。

每次操作你都需要保证序列是单调不减的,你有\(k\)次操作\(1\le k\le n\),当当前长度是\(m\)的时候,贡献是\(\sum_{i =2}^{m}(a_i-a_{i-1})^2\)。问操作\(k\)次的最大值。

思路:一次操作有\(2\)步:$1.删一个数(或者不删)\(2.改一个数。要保证一直是单调不减的,而且还要值最大,那么我们如果要变化一个数,最大的变化范围是\)a[1]\(到\)a[n]$。我们考虑如何使得值最大?

对于\(a->b\),如果中间插入一个\(c\),假设\(a->c\)贡献是\(x\)\(c->b\)贡献是\(y\)

那么对于\(a->b\)的贡献是\((x+y)^2\),而\(a->c->b\)的贡献是\(x^2+y^2\)。显然前者贡献更大。那么我们得到了数越少越优。一开始我们考虑的策略是删中间某几个数,把两边的一遍往小缩小(缩小到它前一个一样),一边往后放大(放大到它后一个一样)。其实换个角度考虑,如果按照以上策略,可以简化一下,因为缩小和放到都是变成和另一个别的什么数一样,那么这个数的贡献变成了\(0\),和删了它效果一样。综上所述,我们要数尽量的少,那么考虑每次都删数,改数也理解为删数,那么题目变成了每次都删,一次删\(2\),第一个和最后一个是不删的,那么最后至少还有\(2\)个数。

对于数怎么删,考虑\(dp\),设\(dp[i][j]\)表示:前\(i\)个数,删\(j\)个(保证这删的\(j\)个不包括第一个和最后一个)。接下来考虑转移:

\(for(i = 1;i<idx;i++)\)

\(cnt = j-((idx-1)-(i+1)+1);\)设这一次删的是\(i+1\)\(idx-1\)

\(dp[idx][j] = \max(dp[idx][j],dp[i][cnt]+(a[idx]-a[i])*(a[idx]-a[i]));\)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 110;
ll n;
ll a[N];
ll dp[N][N];
ll dfs(int idx,int k)
{
	ll &res = dp[idx][k];
	if(dp[idx][k]!=-1)return res;
	res = 0;
	for(int i = 1;i<idx;i++)
	{
		//i+1,idx-1
		int cnt = k-((idx-1)-(i+1)+1);
		if(cnt<0)continue;
		res = max(res,dfs(i,cnt)+(a[idx]-a[i])*(a[idx]-a[i]));
	}
	return res;
}

int main()
{
	cin>>n;
	for(int i = 1;i <= n; i++)
		cin>>a[i];
	memset(dp,-1,sizeof(dp));
	for(int i = 1;i <= n; i++)
	{
		int k = i*2;
		if(n-k-2<=0)cout<<(a[n]-a[1])*(a[n]-a[1])<<"\n";
		else cout<<dfs(n,k)<<"\n";
	}
	return 0;
}
/*
5
1 2 3 4 5
*/

E An Interesting Sequence

题意:给你一个\(a_1 = k\),告诉你数列总长度是\(n\),让你去构造,保证\(\gcd(a_{i-1},a_i)=1\),求\(\sum_{i = 1}^{n}a_i\)的最小值。

思路:对于\(a_1\)是偶数(因为\(3\)不一定和偶数互质),考虑找到与\(a_1\)互质的第一个奇数质数,然后后面构造\(2,3,2,3...\)

对于\(a_1\)是奇数,直接构造\(2,3,2,3,...\)即可(奇数一定和\(2\)互质)。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,k;
const int N =1000010;
bool is_pri[N];
int pri[N];
int cnt;

void init(int n)
{
	memset(is_pri, true, sizeof(is_pri));
	is_pri[1] = false;
	cnt = 0;
	for (int i = 2; i <= n; i++)
	{
		if (is_pri[i])
			pri[++cnt] = i;
		for (int j = 1; j <= cnt && i * pri[j] <= n; j++)
		{
			is_pri[i * pri[j]] = false;
			if (i % pri[j] == 0)break;            
		}
	}
}

int main()
{
	init(20);
	cin>>n>>k;
	ll ans = k;
	if(k%2==0)
	{
		for(int i = 1;i<=cnt;i++)
		{
			if(pri[i]%2)
			{
				if(k%pri[i]!=0)
				{
					ans += pri[i];
					break;
				}
			}
		}
		int m = n-2;
		ans += m/2*5+(m%2)*2;
	}
	else
	{
		int m = n-1;
		ans += m/2*5+(m%2)*2;
	}
	cout<<ans<<endl;
	return 0;
}

F Infinity Tree

题意:一开始你有\(1\)个节点,每秒每个节点产生\(k\)个孩子。即第\(x\)秒的节点个数是\((k+1)^x\)个。

对于一个节点\(y\),它产生的孩子是\([p+(y-1)\times k+1,p+y*k]\)

给你两个节点\(x,y\),问他们的\(lca\)是谁。

思路:考虑对于一个孩子\(x\),它的父亲是谁?根据题目给的公式反推得到父亲\(y = (x-p)/k+((x-p)\mod k!=0)\)

那么我们先预处理出来每秒的节点个数,去找产生节点个数小于\(x\)的最后一个,那就是它父亲那一轮的节点个数,这样可以推出它的父亲。考虑\(x,y\)\(lca\)怎么求?每次跳节点编号大的那个就行。注意,父亲不一定是孩子的上一秒产生的,所以不能单纯的用孩子的秒数-1得到,而是要重新去求。

#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+10;
inline __int128 read(){__int128 x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-'){f=-1;}ch=getchar();}while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}return x*f;}
inline void write(__int128 x){if (x < 0){putchar('-');x = -x;}if (x > 9) write(x / 10);putchar(x % 10 + '0');}
typedef __int128 ll;
ll k,x,y;
ll p[N];
ll now;
ll pre_gx,pre_gy;
void init(ll x)
{

	p[0] = 1;
	now = 1;
	if(x==1)return;
	while(1)
	{
		p[now] = p[now-1]*(k+1);
		if(p[now]>x)break;
		now++;
	}
}

ll lca(ll x,ll y)
{
	
	while(x!=y)
	{
		if(x==1||y==1)return 1;
		if(x>y)
		{
			x = (x-p[pre_gx])/k+((x-p[pre_gx])%k != 0);
			ll t = pre_gx;
			for(int i = t - 1;i>=0;i--)
				if(p[i]<x){
					pre_gx = i;
					break;
				}			

		}
		else if(y>x)
		{
			y = (y-p[pre_gy])/k+((y-p[pre_gy])%k != 0);
			ll t = pre_gy;
			for(int i = t - 1;i>=0;i--)
				if(p[i]<y){
					pre_gy = i;
					break;
				}
		}

	}
	return x;
}


int main()
{
	int t;
	cin>>t;
	while(t--)
	{
		k = read(),x = read(),y = read();
		init(max(x,y));
		
		for(int i = 0;i<=now;i++)
			if(p[i]>x)break;
			else pre_gx = i;
		for(int i = 0;i<=now;i++)
			if(p[i]>y)break;
			else pre_gy = i;
	
		write(lca(x,y));
		cout<<"\n";
	}
	return 0;
}
/*
1
2 100000000000000000 1000000000000000
*/

J A Game about Increasing Sequences

题意:\(A,B\)在玩游戏,每次每人选一个数,要求比上一个人选的大,而且只能从两端选取,第一个选不了了的人输。\(A\)是先手,\(B\)是后手,问谁赢。

思路:因为每次要选比上一个大的,那么选出来的数列一定是单增的。考虑能选多少次,如果前缀和后缀能选的次数都是偶数\(B\)赢,否则\(A\)。因为只要可下次数是奇数,那\(A\)必赢,反之\(B\)。可能你会有疑惑,那我如果某个人选了某一边而导致另一边不能选了的情况,会不会改变奇偶性呢?

举个实际的例子吧:\(1,2,3,5,7,7,5,4,3\)

可能你会疑惑第一次\(A\)选了左边和右边结果会不会不一样?

对应选法就是:(奇数个)\(ABABA\) \(BABA\)(偶数个)

因为都是最优策略,以\(A\)为例,如果\(A\)要自己赢,那么最后一步一定是\(A\)下的,是奇数步数。如果按照只下左边,那么肯定是奇数步,那如果\(B\)改了,他去下右边了,那也不会影响,右边是偶数个,左边下了奇数步,那总的还是奇数步,仍为\(A\)赢。

所以综上所述,只有两边都是偶数才是\(B\)赢。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5+10;
int n,k;
int a[N];
int pre,suf;
int main()
{
	cin>>n;
	for(int i = 1;i<=n;i++)
		cin>>a[i];
	for(int i = 1;i<=n;i++)
	{
		if(a[i]>a[i-1])
			pre++;
		else break;
	}
	for(int i = n;i>=1;i--)
	{
		if(a[i]>a[i+1])
			suf++;
		else break;
	}
	if(pre%2==0&&suf%2==0)cout<<"Bob\n";
	else cout<<"Alice\n";
	return 0;
}