中山市迪茵公学第一届“图灵杯”信息学编程大赛初试题解

我是本次比赛 T2 的出题人

本次比赛没有出现高深的算法,主要在于思维上的比拼

个人认为这次比赛题目质量不错,组题组的也很妙

感谢何教练带来一场优质的比赛

建议开题顺序 T3 -> T1 ->T2 ->T4

周末看能不能搞一个视频题解

T1 小朋友玩游戏

题意

对于一个有正有负的环,求出当中的最大区间和

\(O(n^3)\)做法

枚举所有区间,对于每个区间分别枚举求出区间和,再记录最大值

这种做法期望拿到\(20\)分部分分

请注意题目要求的是一个环,自行思考如何枚举到所有区间,求区间和需要注意什么

\(O(n^2)\)做法

考虑优化求区间和的办法

我们在输入时处理出一个数组\(sum\),\(sum_i\)表示从第一项到第\(i\)项之和

这被称之为前缀和,接下来介绍如何用前缀和求出一个区间的和

如上,如果想求出区间\([L,R]\)的和,显然就是求出黄色部分

我们发现黄色部分正好在\([1,R]\)区间中,而且两区间结尾相同

那么多出来了什么呢?显然是蓝色部分,蓝色部分是什么?

你可能以为是\([1,L]\),那就错了,其实是\([1,L-1]\),第\(L\)项也是\([L,R]\)的一部分,不可忽视

我们通过前缀和求出了所有\([1,i]\)和为\(sum_i\),所以区间和可以这么求

\[sum_{[L,R]}=sum_R-sum_{L-1} \]

这样求的时间复杂度为\(O(1)\),比暴力求好多了

然后还是枚举区间

注意到题目是个环,所以区间可能是这样的

这种情况请大家自行思考

事实上,有两种方式,请大家把两种都想出来

提示:整体减空白

这种做法可以拿到\(40\)

正解:\(O(n)\)做法

其实区间只有两种情况,如下

(P.S.:我的Pinta崩了,所以这里换了一个画图工具,造成前后风格不一致,在此抱歉)

先看看第一种情况

考虑枚举\(R\),再想办法找出一个\(L\),使得\(sum_{[L,R]}\)最大

因为前面我们推导出了

\[sum_{[L,R]}=sum_R-sum_{L-1} \]

\(sum_R\)固定了,我们应该让\(sum_{L-1}\)最小,才能让\(sum_{[L,R]}\)最大

我们干脆记录下\(sum_{1~R-1}\)中的最小值,就可以快速求出\(sum_{[L,R]}\)的最大值0了

这个最小值初始为\(0\),我们可以一边求解一边更新最小值(先求解后更新,题目要求不能不选)

代码如下

for (int i = 1; i <= n; i++) {
        ans = max(ans2, sum[i] - mnsum);
        mnsum = min(mnsum, sum[i]);
    }

解决了第一种情况,来看看第二种情况

其实可以利用整体减空白的思想,让不选的区间的和尽可能小

问题转化成了一个类似于第一种情况的问题了

求解最小区间的原理与最大区间类似

我们应该让\(sum_{L-1}\)最大,记录下\(sum_{1~R-1}\)中的最大值

不过注意题目要求不能不选,所以特判一下,避免不选的区间选择了全部

以上做法满分

#include <bits/stdc++.h>
using namespace std;
long long n, a[1000005], sum[1000005], mxsum , mnsum , ans1 = 1e9, ans2 = -1e9, vis, ans;
int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        sum[i] = sum[i - 1] + a[i];
    }
    for (int i = 1; i <= n; i++) {
        ans1 = min(ans1, sum[i] - mxsum);
        ans2 = max(ans2, sum[i] - mnsum);
        mnsum = min(mnsum, sum[i]);
        if (mxsum <= sum[i]) {
            mxsum = sum[i];
            vis = 1;
        }        

    }
    ans = ans2;
    if ((vis == 1 || ans1 != sum[n]) && sum[n]-ans1 > ans2) {
        ans = sum[n]-ans1;
    }
    cout << ans;
}

奇怪做法:单调队列

破环为链之后,用单调队列维护连续\(n\)\(sum\)中的最小值,(类似滑动窗口)

问题类似于上面的情况一

不懂单调队列的同学可以不管

T2 直角三角形的数量

我校评测机太好了(优于大多数Online Judge),优化的暴力可以满分

如果想要\(O(n\sqrt{n})\)及以下复杂度才能通过的平台,可以选择Luogu

(本人测试,一些在我校平台满分的代码在Luogu只有30分)

链接:T239755 [DuckOI&DiyinOI]直角三角型的数量||简单三角形计数题

然后,因为本人疏忽,本题一个符号打错了,可能给大家带来不便,抱歉

简化题意

大家都知道勾股定理吧,对于一个直角三角形,两直角边平方之和等于斜边的平方

式子表示为\(a^2+b^2=c^2(a,b,c为三角形三边,c为最长边)\)

其实还有一个勾股逆定理,若三角形满足\(a^2+b^2=c^2\),三角形为直角三角形

所以本题可以理解为

已知正整数\(n\),求

\[\begin{cases} x^2+y^2=z^2\\ x<y<z \leq n \end{cases} \]

的所有正整数解

\(O(n^3)\)做法

直接枚举\(x,y,z\),判断是否符合上述式子

这样显然超时

\(O(n^2)\)做法

可以枚举\(x,y\),然后判断对应的\(z\)是不是平方数(平方根是不是整数)

稍微快一点,

\(O(n\sqrt n)\)做法

\[x^2+y^2=z^2 \\ x^2=z^2-y^2 \\ x^2=(z+y)(z-y)//平方差公式\\ \because z>0,y>0 \\ \therefore z+y>z-y \]

考虑对\(x^2\)分解成\(x^2=ab\),其中\(a>x>b>0\)且为正整数
得二元一次方程组

\[\begin{cases} z+y=a\\ z-y=b \end{cases} \]

解得

\[\begin{cases} z=\frac{a+b}2\\ y=\frac{a-b}2 \end{cases} \]

所以有一个要求

\[a \equiv b (mod\; 2)//也就是,要么都是奇数,要么都是偶数 \]

才能使\(z,y\)为整数

对于每一个\(x\),求出\(a,b\),判断以上注意事项即可

如何分解

如果直接分解\(x^2\),需要枚举\(1\)~\(x\),复杂度太高了,我们需要简单的\(O(\sqrt{x})\)做法

可以分解\(x\),在推广到\(x^2\)

memset(cnt,0,sizeof(cnt));
		long long k=x;
		for(int j=2;j<=k;j++)
		{
			if(k%j==0)
			{
				cnt[0][0]++;
				cnt[cnt[0][0]][0]=j;
				while(k%j==0) 
				{
					cnt[cnt[0][0]][1]++;
					k/=j;
				}
				cnt[cnt[0][0]][1]*=2; //to be x^2
			}
		}

然后枚举每一个素因子,复杂度为\(O(ans)\),略高于\(O(\sqrt{n})\)

其实还有一堆优化(甚至可以把难度提升至弱省选难度),在此不多阐述

经过众人讨论,现在发现以下复杂度的做法

\[O(n\sqrt{n}),O(n^{\frac5 4}),O(nlogn) \]

希望有人可以发现\(O(n)\)做法,可以发给我

邮箱: moudengya123@qq.com

代码如下

#include<bits/stdc++.h>
using namespace std;
long long n,x,ans,cnt[20005][2];
void dfs(long long t,long long b)
{
	if(t>cnt[0][0])
	{
		long long a=x*x/b;
		if(a%2==b%2)
		{
			if(x<(a-b)/2&&(a+b)/2<=n)
			{
				ans++;					
			} 
		} 
		return;
	}
	for(int i=0;i<=cnt[t][1];i++)
	{
		dfs(t+1,b);
		b*=cnt[t][0];
	}
} 
int main()
{
	cin>>n;
	for(x=1;x*x*2<=n*n&&x<=n;x++)
	{
		memset(cnt,0,sizeof(cnt));
		long long k=x;
		for(int j=2;j<=k;j++)
		{
			if(k%j==0)
			{
				cnt[0][0]++;
				cnt[cnt[0][0]][0]=j;
				while(k%j==0) 
				{
					cnt[cnt[0][0]][1]++;
					k/=j;
				}
				cnt[cnt[0][0]][1]*=2; 
			}
		}
		dfs(1,1);
	}
	cout<<ans<<endl;
}

其他做法:打表

本题显然可以打表,但不是求出对于每一个\(n\)的解,代码过长会导致编译错误

而是找出\(30000\)以内的基本勾股数的\(z\)(4775项)

再对应每个基本勾股数,求出\(n\)以内派生勾股数的数量

数学名词自行百度

upd:更新了效率更高的代码

这份代码的思路不变,但是预处理出了素数,并用位运算代替了一些操作,等等优化

原先的代码运行了4000ms,本代码只有300ms

#include <stdio.h>
#include <bits/stdc++.h>
using namespace std;
int n, x, ans, cnt[21][2], s[30005], b[1005];
void dfs(int t, int b) {
    if (t > cnt[0][0]) {
        int a = x * x / b;
        if (!((a & 1) ^ (b & 1))) {
            if (x < ((a - b) >> 1) && ((a + b) >> 1) <= n) {
                ans++;
            }
        }
        return;
    }
    for (int i = 0; i <= cnt[t][1]; i++) {
        dfs(t + 1, b);
        b *= cnt[t][0];
        if (b * b > x * x)
            return;
    }
}
int main() {
    cin >> n;
    s[1] = 1;
    for (int i = 2; i <= 25000; i++) {
        if (s[i] == 0) {
            b[++b[0]] = i;
            for (int j = i + i; j <= 25000; j += i) {
                s[j] = 1;
            }
        }
    }
    for (x = 3; x * x * 2 <= n * n && x <= n; x++) {
        memset(cnt, 0, sizeof(cnt));
        int k = x;
        for (int j = 1; j <= k; j++) {
            if (!((k % b[j]) ^ 0)) {
                cnt[0][0]++;
                cnt[cnt[0][0]][0] = b[j];
                while (!((k % b[j]) ^ 0)) {
                    cnt[cnt[0][0]][1]++;
                    k /= b[j];
                }
                cnt[cnt[0][0]][1] <<= 1;
            }
        }
        dfs(1, 1);
    }
    cout << ans << endl;
}

T3 奖金设置

一道简单枚举题,跳过

本人代码有一些优化,事实证明不优化也可以

#include <bits/stdc++.h>
using namespace std;
long long n,m,ans;
int main() {
    cin>>n>>m;
    if(m%100==0)
    {
        m/=100;
    }
    else
    {
        cout<<0<<endl;
    }
    n=round(n*1.000000/5);//四舍五入函数
    n-=1;
    m-=30;
    for(int A=1;A<=n;A++)
    {
        for(int B=A;A+B<=n;B++)
        {
            int C=n-A-B;
            if(C<B)continue;
            for(int a=30;a>=1;a--)
            {
                for(int b=a;b>=1;b--)
                {
                    int c=m-A*a-B*b;
                    if(c<=0)continue;
                    if(c%C!=0)continue;
                    c/=C;
                    if(c>b)continue;
                    ans++;
                }
            }
        }
    }
    cout<<ans;
}

T4 游戏

思维题,好题

首先化简分数

\[x=\frac n m \]

设拿分值为\(i\)的牌的数量为\(a_i\),易得方程组

\[\begin{cases} a_1+2a_2+3a_3+4a_4+5a_5=n\\ a_1+a_2+a_3+a_4+a_5=m\\ \end{cases} \]

显然有多解

这里提供一种构造方法,只拿\(\lfloor x\rfloor\)(向下取整)和\(\lceil x\rceil\)的牌(向上取整)

比如:\(1.35\),就只拿\(1\)\(2\)

假设一开始只拿某一种牌,然后通过看与\(x\)的差

渐渐将一些牌改成另一种牌

其实就是小学奥数的假设法

先认为全是某一种东西

然后再比对与想要结果的差进行修改

一个更直接的说法:鸡兔同笼

#include<bits/stdc++.h>
using namespace std;
#define ll long long
double n;
ll a,b,g,k,ans[11];
ll gcd(ll x,ll y) { return x%y==0?y:gcd(y,x%y); }
int main() {
    cin>>n;
    a=(ll)(n*1000000000),b=1000000000;
    g=gcd((ll)(n*1000000000),1000000000);
    a/=g,b/=g;
    for(int i=5;i>=1;i--) {
        if(b*i<a) {
            k=i;
            break;
        }
    }
    int x=a-b*k;
    //cout<<a<<' '<<b<<' '<<k<<' '<<x<<endl;
    ans[k]=b-x,ans[k+1]=x;
    for(int i=1;i<=5;i++) {
        cout<<ans[i]<<' ';
    }
    return 0;
}

代码来自于Huangzixin大佬,本人还没有调出来,只有\(92\)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值