2020-2021 ICPC Southeastern European Regional Programming Contest

  1. Reverse Game 博弈、思维

大意:给定一个01字符串S,两人轮流操作,每次可以选择“10”、“110”、“100”、“1010”进行翻转。不能操作的判输。问谁会获胜?

思路:题目给的东西都不是随便给的,观察上面给出的几个字符串我们就会发现,最终不能继续进行操作的时候一定是前半部分全部是0后半部分全部是1。因为一旦1后面有0,那么必然可以翻转将0往前面翻,直到翻到前面不再有1为止。每次只翻转10的次数是把每个1后面0的个数加起来。然后我们观察发现,每次翻转,最少少1个、最多少2个“10”。“1010”翻转少了三个,但是变成“0101”又多了一个,所以还是少2个,所以相当于取石子的问题,把 “10” 看成石子,3是关键数字,所以只要不是3的倍数先手必胜。

代码如下:

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	string s;
	cin>>s;
	LL cnt=0,k=0;
    
	for(int i=0;i<s.size();i++)
	{
		if(s[i]=='0')cnt+=i-k,k++;
	}
	if(cnt%3)cout<<"Alice\n";
	else cout<<"Bob\n";

	return 0;
}

总结:题目给定的信息都是设计好的,既然给了,就肯定会有特别的意义。

  1. Divisible by 3 思维、dp

大意:给定一个序列 a,定义区间[l,r]的权重为 1 < = i < j < = r 1<=i<j<=r 1<=i<j<=r b i b j b_ib_j bi​bj​乘积之和,问一共有多少个权重%3为0的区间。

思路:我们之前做过简单的区间和%3为的区间个数。那个题的做法是求出前缀和%3的,对于区间满足sum[l,r]%3为0即是sum[r]-sum[l-1]=0。

也就是说对于遍历到当前位置r时,我们需要找前面有多少个 l 是和 sum[r]相等的,我们可以开个一维数组, 从前往后扫一遍边更新计数就行了。对于这道题完全可以类比过去,唯一不同的就求和方式不同。赛时没推出式子。没写出。

对于简单区间和的时后我们只需要开个一维的数组就行了。但是对于此题,经过我们一系列的推导,最终推出的式子为

w [ i ] = w [ i − 1 ] + a [ i ] ∗ s u m [ i − 1 ] w[i]=w[i-1]+a[i]*sum[i-1] w[i]=w[i−1]+a[i]∗sum[i−1]

w [ l , r ] = w [ r ] − w [ l − 1 ] − s u m [ l − 1 ] ∗ ( s u m [ r ] − s u m [ l − 1 ] ) w[l,r]=w[r]-w[l-1]-sum[l-1]*(sum[r]-sum[l-1]) w[l,r]=w[r]−w[l−1]−sum[l−1]∗(sum[r]−sum[l−1])。我们需要同时知道w,sum。所以开个二维数组来计数,w,sum值只有0,1,2直接枚举就行了。

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N=500010;
typedef long long LL;
int n;
LL a[N],cnt[3][3],w[N],sum[N];
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];

	LL ans=0;
	cnt[0][0]=1;

	for(int i=1;i<=n;i++)
	{
		w[i]=(w[i-1]+a[i]*sum[i-1])%3;

		sum[i]=(sum[i-1]+a[i])%3;

		for(int j=0;j<3;j++)
		{
			for(int k=0;k<3;k++)
			{
				int t=((w[i]-j-(sum[i]-k)*k)%3+3)%3;
				if(!t)ans+=cnt[j][k];
			}
		}
		
		cnt[w[i]][sum[i]]++;
	}
	cout<<ans<<"\n";
	return 0;
}

总结:对于相似的问题,不妨从简单的入手,然后慢慢地类比。

  1. Mistake 签到、直接用map 就行了
  2. Modulo Permutations 思维、dp

大意:给定一个 n 的排列,问能构造出多少种方案使得 p i m o d      p i + 1 < = 2 p_i\mod\ p_{i+1}<=2 pi​mod pi+1​<=2。 p n + 1 = p 1 , n < = 1 e 6 p_{n+1}=p_1,n<=1e6 pn+1​=p1​,n<=1e6 结果对1e9+7 取模

思路:首先我们得明白给定 p n + 1 = p 1 p_{n+1}=p_1 pn+1​=p1​ 的用意,实际上就是个环,对于环我们一般是拆成链写。对于其中1、2两个数是非常特殊的。

除了这两个数,其他的数必须得递减排序,否则构不成 p i m o d      p i + 1 < = 2 p_i\mod\ p_{i+1}<=2 pi​mod pi+1​<=2 。我们通过 1或2把环拆成一条链。对于一条链也可以根据1、2分成两段递减的排列。经过一系列转化我们就将问题变成了将 3-n 的数字分成两段递减的排列,并且满足上述条件。因为构成的是一个环,最终的方案数再乘n。接下来就是怎么求同时考虑两段的方案数。显然两段是等价的,我们只需要对其中一段考虑完所有的可能就好了。我们用dp来写,从大到下枚举x。我们用f(x,i,j)表示现在用到的数为x,第一段的数末尾为i,第二段的数末尾为 j,显然i,j必有一个为 x,因为两段实际上是等价的,我们对其中的一段考虑完所有情况就行了,所以不妨把第一段的末尾当成x即 f(x,j)。接着向后转移,我们先不考虑合不合法,x-1放第一段或者第二段,对于 f(x-1,j), f(x,x-1)。对于第一个转移必然是合法的,相当于直接让f(x-1,j)=f(x,j)。省掉第一维之后可以直接忽略。对于第二个转移,j=k*(x-1)+r, 0<=r<=2,但是对于第二个转移实际上是f(x,x-1),我们要省去第一维的话要保证第一维是一致的,所以改成f(x-1,x),也就是所我们用的是x-1,但是dp更新的是x,这样两种转移就都可以都省掉第一维。对于第二个转移就是枚举倍数,枚举倍数k=0,r=0时,则值为0,相当于不加数,也是一种方案,所以得有dp[0]=1,枚举倍数是调和级数,时间复杂度logn。总的时间复杂度就是 O ( n l o g n ) O(nlogn) O(nlogn)。因为1,2两个数放在一段的开头是两种情况,所以别忘了乘2。

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N=1e6+10,M=1e9+7;
typedef long long LL;
LL f[N];
int n;
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n;
    if(n<=2)
    {
        cout<<n<<"\n";
        return 0;
    }
    f[0]=1;
    for(int i=n-1;i>2;i--)
    {
        LL v=0;
        for(int j=0;j<=n;j+=i)
            for(int k=0;k<3&&j+k<=n;k++)
                v+=f[j+k];
    
        f[i+1]=v%M;
    }
    LL ans=0;

    for(int i=0;i<=n;i++)ans=(ans+f[i])%M;

    cout<<ans*2*n%M<<"\n";
    return 0;
}

总结:纪录方案数直接排列组合不好求的时候考虑dp来写。

上一篇:Codeforces Harbour.Space Scholarship Contest 2021-2022/1553E Permutation Shift


下一篇:UCF Local Programming Contest Round 解题/补题报告