SF Round 4

SF Round 4

$$\lceil \text{SF Round 4} \rfloor$$

愿薪火相传,美德不灭

赛后总结

经过 $4$ 个小时的比赛,SF Round 4 落下帷幕。

本场比赛共有 $5$ 人参加,共有 $0$ 人 AK,$5$ 人取得有效分数。

本场比赛作为 NOIP 前的信心赛,题目较简单、知识较单一,着重考察选手的思维能力与代码能力。


题解

A. SheKong hates equations

本题延续了 $\text{SF Round}$ 中 T1 必为大模拟的优良传统。

(详见 [SF Round 0] 〇.[SF Round 1] ESP_8266[SF Round 2] 南鸣卖菜

题目要求我们解两个处理过的二元一次方程,那么很显然,主要的实现就是处理系数处理字母代入公式计算三步。

那么需要注意的有以下细节:

  • 系数为 $\pm 1$ 时,$1$ 是省略的。
  • 方程中的第一个系数不会带正号 $+$。
  • 可能存在一个方程中只有一个未知数。
  • 两个方程的未知数出现顺序可能不同。
  • 题面中提到:出现的数字均在 $\texttt{int}$ 范围内,但在运算中可能爆 $\texttt{int}$。
  • 对 $-0.000$ 要特判为 $0.000$。
  • 注意两个系数都为负数的方程。

更多细节详见代码。代码实现如下:

//Author: SheKong

#include<cstdio>
#include<cstring>
#include<ctype.h>
long long getxs(char c,char s[])
{
	int i,len=strlen(s),f=1;
	long long res=0;
	for(i=0;i<len;i++)
	{
		if(!isdigit(s[i]))
		{
			if(s[i]=='-')f=-1;
			else if(s[i]=='+')f=1;
			else if(s[i]==c)
			{
				return res?res*f:f;
			}
			else res=0;
		}
		else res=res*10+s[i]-'0';
	}
	return 0;
}
long long getcs(char s[])
{
	int i=0,len=strlen(s),f=1;
	long long res=0;
	while(s[i]!='=')i++;
	for(i++;i<len;i++)
	{
		if(!isdigit(s[i])&&s[i]=='-')f=-1;
		else res=res*10+s[i]-'0';
	}
	return res*f;
}
int main()
{
	char s1[50],s2[50],x1='0',x2='0';
	scanf("%s%s",s1,s2);
	long long xsx1,xsy1,xsx2,xsy2,cs1,cs2;
	int i,len1=strlen(s1),len2=strlen(s2);
	for(i=0;i<len1;i++)
	{
		if(s1[i]>='a'&&s1[i]<='z')
		{
			if(x1=='0')x1=s1[i];
			else if(x2=='0')x2=s1[i];
		}
	}
	for(i=0;i<len2;i++)
	{
		if(s2[i]>='a'&&s2[i]<='z')
		{
			if(x1=='0')x1=s2[i];
			else if(s2[i]!=x1&&x2=='0')x2=s2[i];
		}
	}
	xsx1=getxs(x1,s1),xsy1=getxs(x2,s1),cs1=getcs(s1);
	xsx2=getxs(x1,s2),xsy2=getxs(x2,s2),cs2=getcs(s2);
	long double a1=xsx1,a2=xsx2,b1=xsy1,b2=xsy2,c1=cs1,c2=cs2,x,y;
	if(a1*b2-a2*b1!=0)x=(c1*b2-c2*b1)/(a1*b2-a2*b1);
	if((c1*b2-c2*b1)==0&&(a1*b2-a2*b1)==0)x=0;
	if(b1!=0)y=(c1-a1*x)/b1;
	else y=(c2-a2*x)/b2;
	printf("%c=%.3Lf\n%c=%.3Lf",x1,x==-0?0:x,x2,y==-0?0:y);
	return 0;
}

B. 奖学金(scholarship)

写在前面

这道题的背景是真实的。

一个芒种的手办榨干了我中考三次模拟的奖学金。

40pts

这题我(赛前)实在没想到部分分的做法,所以直接上正解的思路了。

(如果赛中发现了只得了部分分的代码我会在评讲时单独拿出来分析的。)

100pts

这道题需要我们一个递归的思路:

我们以样例2 $(m=19)$ 举例:

  • 如果我们能表示出 $1$~$9$ 的所有数,那么我们只需要再有一个 $10$ 就能表示出 $1$~$19$ 的所有数。

  • 如果我们能表示出 $1$~$4$ 的所有数,那么我们只需要再有一个 $5$ 就能表示出 $1$~$9$ 的所有数。

  • 如果我们能表示出 $1$~$2$ 的所有数,那么我们只需要再有一个 $2$ 就能表示出 $1$~$4$ 的所有数。

  • 如果我们能表示出 $1$~$1$ 的所有数,那么我们只需要再有一个 $1$ 就能表示出 $1$~$2$ 的所有数。

由这个思路,我们可以构造出样例中的方案:$1,1,2,5,10$。

由上,我们不难推广到 $m$ 为任意正整数的情况:

  • 如果我们能表示出 $1$~$\lfloor \frac{n}{2} \rfloor$ 的所有数,那么我们只需要再有一个 $\lceil \frac{n}{2} \rceil$ 就能表示出 $1$~$n$ 的所有数。

  • 如上我们不断递归,直到对半后向上取整的结果为 $2$ 或 $3$ 为止(分别对应到最小两项为 $(1,1),(1,2)$的情况)。

代码实现上,其实并不需要用到递归。只需用循环不断做对半操作并逆序输出即可。

核心代码如下:

//Author: WalkerV

int m,cnt;
int a[50];

void Solve() {
	while(m) {
		a[++cnt]=abs((m+1)/2);
		m/=2;
	}
	printf("%d\n",cnt);
	return;
}

C. 人口迁移

更高质量的题解传送门:[隐藏在汉诺塔中的分形曲线] by 3Blue1Brown

此题的本质就是一个汉诺塔,只不过每次移动只能在相邻的柱子中进行。

题目的主要思想已经在上面的视频里了。 (别问,问就是不想写)

注意到答案里会出现 $3^n$,而题目的 $n$ 范围最高达到了 $10^{18}$,所以如果只使用平时 $O(n)$ 的计算方式的话会 TLE ,所以此处再介绍一个算法:快速幂,可以以 $O(\log n)$ 的复杂度进行幂运算。

结合上面的思想和算法,得出此题 AC 代码:

//Author: SheKong

#include<bits/stdc++.h>
long long MOD=998244353;
inline long long abs(long long a){return a<0?-a:a;}
long long qpow(long long b,long long k,long long p)//快速幂
{
    long long ans=1;
    while(k>0)
    {
        if(k&1)ans=((ans%p)*(b%p))%p;
        k>>=1;
        b=((b%p)*(b%p))%p;
    }
    return ans;
}
int main()
{
	long long l,n,r,ans=1,k=3;
	scanf("%lld %lld %lld",&l,&n,&r);
	while(n)
	{
		if(n&1)ans=ans*k%MOD;
		k=k*k%MOD;
		n>>=1;
	}
	printf("%lld",abs(r-l)==1?(ans-1+MOD)*qpow(2,MOD-2,MOD)%MOD:(ans-1+MOD)%MOD);
	return 0;
}

D. 波动(flow)

写在前面

这道题的背景是在河南暴雨时想到的,谨以此题向每一位抗洪英雄致敬。

至于模数 $19980928$,那是98年特大洪水时,宣布抗洪抢险斗争已经取得全面胜利的日子。

10pts

根据题意,我们可以枚举出所有的数列并判定是否符合性质。

时间复杂度为 $O(2^n \times n^3)$。

核心代码如下:

//Author: WalkerV

#define MOD 19980928

int a[30];
long long n,ans;

void Check() {
	int sum;
	for(int k=1;k<=n;k++) {
		for(int m=k;m<=n;m++) {
			sum=0;
			for(int i=k*2-1;i<=m*2;i++) {
				sum+=a[i];
			}
			if(sum<-2||sum>2) {
				return;
			}
		}
	}
	ans++,ans%=MOD;
	return;
}

void Recur(int dep) {
	if(dep==n*2+1) {
		Check();
		return;
	}
	for(int i=1;i<=2;i++) {
		if(i==1) {
			a[dep]=-1;
			Recur(dep+1);
		}
		else {
			a[dep]=1;
			Recur(dep+1);
		}
	}
}

void Subtask1() {
	Recur(1);
	return;
}

30pts

由题意知,对于任意的 $i(1 \leq i \leq n)$,$x_{2i-1}$ 与 $x_{2i}$ 要么相同,要么不同。

那我们考虑这样一个数列 ${ y_n }$,满足 $y_i=x_{2i-1}+x_{2i}$,则显然 $y_i \in { -2, 0,2 }$。

则原条件$\left| \sum_{i=2k-1}^{2m} x_i \right| \leq 2$ 等价于 $\left| \sum_{i=k}^{m} y_i \right| \leq 2$。

当 $y_i= \pm 2$ 时,我们称 $i$ 为波动点,否则称其为非波动点

由条件知,若 $i, j$ 为相邻的两个波动点,则 $y_i+y_j=0$。故波动点对应的项必然为 $-2$ 与 $2$ 交替出现(即 $y_i=-2, y_j=2$ 或 $y_i=2, y_j=-2$),则全体波动点的可能性共有两种(即第一个波动点对应项为 $-2$ 或 $2$,后面的所有波动点对应项都由第一个波动点所确定)。

接下来按波动点的数量分类讨论:

  • $0$ 个波动点
    对于每一个 $y_i$ 所对应的 $(x_{2i-1},x_{2i})$,可以是 $(-1,1), (1,-1)$的任意一种。
    共 $n$ 个位置,故总情况有 $2^n$ 种。

  • $p$ 个波动点 $(p \geq 1)$:
    在总共 $n$ 个点中选出 $p$ 个点为波动点,情况有 $C_n^p$ 种;
    波动点有 $2$ 种情况;
    对于剩下的 $(n-p)$ 个非波动点,每个非波动点对应的 $(x_{2i-1},x_{2i})$,可以是 $(-1,1), (1,-1)$的任意一种,情况共 $2^{n-p}$ 种。
    综上,总情况有 $C_n^p \times 2 \times 2^{n-p}$ 种。

结合以上两种情况,答案为 $2^n+ \sum_{p=1}^n (C_n^p \times 2 \times 2^{n-p})$。

代码实现上,需要 $O(n^2)$ 的时间复杂度处理出组合数。

60pts

对于题目中的数列 ${ x_{2n} }$,定义长度为 $l$ 的前缀和 $S_l= \sum^l_{i=1} x_i$。

显然 $S_{2i} \in { -2, 0,2 }(1 \leq i \leq n)$。

下证,对于一个符合题目条件的数列,不存在两个不同的整数 $i,j(1 \leq i,j \leq n)$,使得 $S_{2i}=2$ 且 $S_{2j}=-2$

证明:

假设命题不成立,即对于一个符合题目条件的数列,存在两个不同的整数 $i,j(1 \leq i,j \leq n)$,使得 $S_{2i}=2$ 且 $S_{2j}=-2$。

  • 当 $i<j$ 时,对于整数 $k=i+1,m=j$,有 $\sum_{i=2k-1} ^{2m}x_i=S_{2j}-S_{2i}=-4$,即有 $\left| \sum_{i=2k-1}^{2m} x_i \right| >2$。矛盾!

  • 当 $i>j$ 时,同理。

综上,命题得证。

故原问题可分为两种情况:

  • $S_{2i} \in { 0,2 }(1 \leq i \leq n)$
  • $S_{2i} \in { -2, 0 }(1 \leq i \leq n)$

显然这两种情况是等价的,我们考虑第一种情况。

定义状态 $f(l,S_{2l})$ 表示:满足长度为 $2l$,$x_{i} \in { -1,1 } (1 \leq i \leq 2l)$;对任何整数 $k, m(1 \leq k \leq m \leq l)$,有 $\left| \sum_{i=2k-1}^{2m} x_i \right| \leq 2$ 的数列 ${ x_{2l} }$ 个数。

通俗的说,就是按题目要求从第一项开始构造数列,构造到第 $2l$ 项且 $s=S_{2l}$ 的方案数。

在这个状态的定义下,第一种情况的答案是 $f(n,0)+f(n,2)$,且有 $f(0,0)=1,f(0,2)=0$。

  • 考虑状态 $f(l,0)$。我们发现在按题目要求进行构造时,一个长度为 $2l$,$S_{2l}=0$ 的数列可以由一个长度为 $2(l-1)$,$S_{2(l-1)}=0$ 的数列添加 $1,-1$ 或 $-1,1$ 两项得到;或者由一个长度为 $2(l-1)$,$S_{2(l-1)}=2$ 的数列添加 $-1,-1$ 两项得到。
  • 考虑状态 $f(l,2)$。我们发现在按题目要求进行构造时,一个长度为 $2l$,$S_{2l}=2$ 的数列可以由一个长度为 $2(l-1)$,$S_{2(l-1)}=2$ 的数列添加 $1,-1$ 或 $-1,1$ 两项得到;或者由一个长度为 $2(l-1)$,$S_{2(l-1)}=0$ 的数列添加 $1,1$ 两项得到。

状态 $f(l,S_{2l})$ 满足以下两个转移方程

  • $f(l,0)=2 \times f(l-1,0)+f(l-1,2)$
  • $f(l,2)=2 \times f(l-1,2)+f(l-1,0)$

从 $f(0,0),f(0,2)$ 递推即可。

因为有两种情况,故将得到的答案乘 $2$。

又注意到,这两种情况里,有一类数列被考虑了两次。即 $S_{2i}=0(1 \leq i \leq n)$ 的数列。

思路如30分做法中“$0$ 个波动点”的情况,可知这一类数列共有 $2^n$ 个。

综上,原题目答案应为 $2 \times [f(n,0)+f(n,2)]-2^n$。

代码实现上,我们用二维数组 $\texttt{f}$ 表示状态 $f$,其中 f[l][0] 表示 $f(l,0)$,f[l][1] 表示 $f(l,2)$。

时间复杂度为 $O(n)$。

这里还需要注意一下空间复杂度为 $O(n)$。鉴于 256MB 能开大约 $6 \times 10^7$ 个 $\texttt{int}$ 型变量,故空间上应当没有问题。还可以进一步将二维数组 $\texttt{f}$ 优化为 $4$ 个 $\texttt{int}$ 型变量。

核心代码如下:

//Author: WalkerV

#define MOD 19980928
#define N 10000010

long long n,ans;
long long f[N][2];

void Subtask2() {
	f[0][0]=1,f[0][1]=0;
	for(int i=1;i<=n;i++) {
		f[i][0]=2*f[i-1][0]+f[i-1][1];
		f[i][0]%=MOD;
		f[i][1]=f[i-1][0]+2*f[i-1][1];
		f[i][1]%=MOD;
	}
	ans=2*(f[n][0]+f[n][1])-Quickpow(2,n,MOD);
	ans%=MOD;
	if(ans<0) {
		ans+=MOD;
	}
	return;
}

100pts

对30pts做法的改进

观察30分做法中我们得到的式子:$2^n+ \sum_{p=1}^n (C_n^p \times 2 \times 2^{n-p})$。

做以下变形:

$2^n+ \sum_{p=1}^n (C^p_n \times 2 \times 2^{n-p}) \ =2^n+2 \times \sum_{p=1}^n (C_n^{n-p} \times 2^{n-p})$

注意到对于 $\sum_{p=1}^n (C_n^{n-p} \times 2^{n-p})$ 这一部分,利用二项式定理可以如下变形:

$ \sum_{p=1}^n ( C_n^{n -p} \times 2^{n -p}) \ = \sum_{p=0}^n (C_n^{n -p} \times 1^p \times 2^{n -p}) -C_n^{n -0} \times 1^0 \times 2^{n -0} \ = (1+2)^n -2^n \ = 3^{n} -2^{n}$

故原式继续变形得:

$2^n+2 \times \sum_{p=1}^n (C_n^{n-p} \times 2^{n-p}) \ =2^n+2 \times (3^n -2^n) \ =2 \times 3^{n} -2^{n}$

故答案为 $2 \times 3^{n} -2^{n}$。

代码实现上,利用快速幂,时间复杂度为 $O(\log n)$。

核心代码如下:

//Author: WalkerV

#define MOD 19980928

long long n,ans;

long long Quickpow(long long x,long long p,int mod) {
	long long ret=1;
	if(p==0) {
		ret=1%mod;
		return ret;
	}
	while(p) {
		if(p%2==1) {
			ret*=x,ret%=mod;
		}
		x*=x,x%=mod;
		p/=2;
	}
	return ret;
}

void Subtask3() {
	ans=2*Quickpow(3,n,MOD)-Quickpow(2,n,MOD);
	ans%=MOD;
	if(ans<0) {
		ans+=MOD;
	}
	return;
}
对60pts做法的改进
法一:数学方法

枚举 $f(l,S_{2i})$ 的前几项:

$i$ $f(i,0)$ $f(i,2)$ $f(i,0)+f(i,2)$
$0$ $1$ $0$ $1$
$1$ $2$ $1$ $3$
$2$ $5$ $4$ $9$
$3$ $14$ $13$ $27$
$4$ $41$ $40$ $81$

由上表,我们可以做出如下猜想:

  • $f(i,0)+f(i,2)=3^i$

下证 $f(i,0)+f(i,2)=3^i$:

当 $i=0$ 时,$f(0,0)+f(0,2)=3^0$。

假设当 $i=k(k \geq 1)$ 时命题成立,即 $f(k,0)+f(k,2)=3^k$。

当 $i=k+1$ 时,

$f(k+1,0)+f(k+1,2) \ =[2 \times f(k,0)+f(k,2)]+[2 \times f(k,2)+f(k,0)] \ =3 \times [f(k,0)+f(k,2)] \ =3 \times 3^k \ =3^{k+1}$

综上,命题得证。

故答案为 $2 \times 3^{n} -2^{n}$。

代码实现同对30分做法的改进。

法二:矩阵快速幂

利用矩阵快速幂,可以将60分做法中的递推的时间复杂度直接优化到 $O(\log n)$。

代码实现如下:

//Author: yussgrw

#include <cstdio>
#include <cstring>

const int P = 19980928;

struct Matrix {
  int m[3][3];
  int r, c;
  Matrix() { memset(m, 0, sizeof(m)); }
};

Matrix operator*(Matrix x, Matrix y) {
  int i, j, k;
  Matrix res;
  for (i = 1; i <= x.r; i++) {
    for (j = 1; j <= y.c; j++) {
      for (k = 1; k <= x.c; k++) {
        res.m[i][j] = (res.m[i][j] + (long long)x.m[i][k] * y.m[k][j]) % P;
      }
    }
  }
  res.r = x.r;
  res.c = y.c;
  return res;
}

Matrix quickpow(Matrix x, long long y) {
  Matrix res;
  res.r = res.c = 2;
  res.m[1][1] = res.m[2][2] = 1;
  while (y) {
    if (y & 1) {
      res = res * x;
    }
    x = x * x;
    y >>= 1;
  }
  return res;
}

int quickpow(int x, long long y) {
  int res = 1;
  while (y) {
    if (y & 1) {
      res = (long long)res * x % P;
    }
    x = (long long)x * x % P;
    y >>= 1;
  }
  return res;
}

int main() {
  Matrix res, x;
  res.r = 2;
  res.c = 1;
  res.m[1][1] = 1;
  x.r = x.c = 2;
  x.m[1][1] = x.m[2][2] = 2;
  x.m[1][2] = x.m[2][1] = 1;
  long long N;
  scanf("%lld", &N);
  res = quickpow(x, N) * res;
  int ans = (((res.m[1][1] + res.m[2][1]) * 2 - quickpow(2, N)) % P + P) % P;
  printf("%d\n", ans);
  return 0;
}

写在最后

作为 $\text{SF Round 4}$ 的压轴题,我更想通过这道题让大家领会一些在 OI 中做题的经验

  • 在一道 OI 的题目上,往往会设置多档部分分。应当根据不同部分分所对应的数据规模来确定对应算法的复杂度,并据此猜想应当使用的算法。
  • 在一道题不能直接得到正解的时候,不妨从部分分入手。很多时候正解都是对部分分算法的优化或改良。
  • OI 中不需要非常严谨的数学推导,但这并不代表着不需要数学功底。事实上,数学功底是许多选手在提升自己时所遇到的瓶颈之一。
  • 枚举永远是有用的。在 OI 中,这并不只是意味着对诸如此题的数列枚举,还可以推广到对小规模数据的手工验算。这对于理解题意寻找突破口是有帮助的。
  • 尝试多角度切入题目。虽然一道题目的多种解法应当是本质相同的,但在 OI 中不乏从完全不同的思路都能走通的题目。比如本题的两种思路分别对应到数学知识动态规划两种基本思路上。类似的还有这道题,它利用了图论暴力两种基本思路。
posted @ 2021-09-12 17:48  WalkerV  阅读(49)  评论(0编辑  收藏  举报