速通ACM省铜第一天 赋源码(The Cunning Seller (hard version))

发布于:2025-09-12 ⋅ 阅读:(14) ⋅ 点赞:(0)

目录

引言:

正文:

        题意分析

        逻辑梳理

        代码实现

        转三进制与交易次数和钱的预处理

        进制退位(核心)

        源代码

        错误分析

        san数组重置的重要性

        源代码

        错误样例

        为何这题数组重制很重要

        超时问题

        源代码

        分析

结语:


引言:

        ACM临近了,那么C语言的部分就暂时先不更新了,我们来通过一个月的训练,争取拿一个ACM的省铜,那么,之后这一个月的时间里,我所发的博客的题目来源于codeforces(即cf)中,难度区间呢,在1300-1500之间,将这个难度区间的题基本能打出来,那么省铜一般也是稳了,那么,怎么筛选难度呢,在这里,在俩个框框中输入难度区间即可,如图

        那么,我们选完填完1300-1500后,即可开始刷题

        今日,我们要讲的是难度评定在1400的一道题The Cunning Seller (hard version)

        这道题真的挺折磨我的,思路其实是好想的,但是细节方面还是比较多的,也可能是我太菜了,给你们看看我连wa9次的超神战绩

        接下来,我们就来讲一讲这个题,顺便讲讲我踩的种种坑————>

        注:解题用的C++

                                                        

正文:

        那么,我们正式开始讲解这道题,我们先来分析一下题目

        题意分析

        这是题目链接Problem - 2132C2 - Codeforces

        不想跳转可看下图

        那么,我们可以先结合题目与样例的解说可以得到,题目中的 n 表示需要买的西瓜,k 表示可以分多少批来购买西瓜,然后每次买西瓜的数量一定要是3的指数次幂,即3的x次。然后题目问我们最终是否可以在指定的批次内买到指定的西瓜数量,并且要使花的钱最少,那么,这就是这个题目的主要要求

        接下来,我们对该要求的实现进行逻辑推理,然后再将推理出的逻辑用代码表示出来即可


        逻辑梳理

        首先,我们来看一下买不同指数幂的西瓜跟价钱的关系,如图

        

        通过上述的推算,我们可以知道,在同样的西瓜的数量下,若指数幂为x+1级,则需要3个指数幂为x级来等价,通过运算可知,x+1的指数幂所花的钱比x指数幂所花的钱多3的x指数幂,且西瓜数量是一样的,所以指数次数越低,所花的钱就越少。

        那么,接下来,我们来想次数的关系,我们可以通过将n转为三进制,这样,就可以得出若要购买n西瓜所需要的最小交易次数,即各个位上的数相加(原因也在上述推算中得出,三个低指数幂西瓜量等于一个高指数幂西瓜量),然后通过最小交易次数的比较,来判断是否可以达成交易,

        若能达成,就对三进制进行压缩,即把一个高位,拆成对应的3个低位(即多俩次交易次数),然后减去便宜了多少钱,一直压缩至次数压缩完或已经压缩到最小

        若不能达成,只需要输出"-1"即可

        那么逻辑已经整理完了,我们来讲一讲代码的实现


        代码实现

        我们通过逻辑梳理,可以总结出,其实要实现这个代码需要注意的只有俩个部分,一个便是将n变为三进制并存起来,还有一个便是对三进制的n进行操作,那么我们开始教学

        首先我们在全局变量处写上t,n,k,以及数组 san[100]

        t,n,k        顾名思义,就是样例个数,要买的西瓜数和可交易次数

        san[100]        即将n转换为三进制后存放的位置,为什么设为100,因为n的数据范围到1e9,但2的31次就跟1e9差不多了,那么3的指数幂肯定比2更快到1e9,但为了美观,所以我写了100

        转三进制与交易次数和钱的预处理

        接下来就是将n转变为三进制后存入数组即可,这部分代码如下,那我们来解释一下这个代码 

memset(san, 0, sizeof(san));
int wei = 0;
cin >> n >> k;
for (wei = 0; pow(3, wei) <= n; wei++);
wei--;
int tou = wei;
long long ci = 0;
long long ans = 0;
while (n)
{
	int i = 0;
	for (i = 0; i <= 3; i++)
	{
		if (i * pow(3, wei) > n)
			break;
	}
	i--;
	ci += i;
	ans += i * (pow(3, wei + 1) + wei * pow(3, wei - 1));
	san[wei] = i;
	n -= i * pow(3, wei);
	wei--;
}

        首先对san数组进行预处理(必须进行预处理,因为不预处理会出事,具体我会在我的错误分析中讲解)

        然后wei代表的是n转变成三进制后的最高位,随后输入n和k,通过循环先找到最高位用wei表示

        然后用tou给wei赋值,用于之后的对进制压缩操作,为什么还要再赋一个值,因为wei在之后要进行操作, 会导致使用出现问题,所以用tou给wei赋值

        随后俩个变量

        ci        表示交易的次数

        ans     表示需要花费的钱

        随后用一个while循环,将n转换为三进制,并算出最少交易次数ci以及在最少交易次数时所花的钱ans

在三进制存完以及各类变量与处理完后,就可以进行核心代码(即进制退位)的编写了

        进制退位(核心)

        接下来就是最核心的进制退位过程了,因为我们已经得出一个高指数幂的数量相当于3个低指数幂的和,但金钱相差是3的低指数幂次,所以可以进行退位,以一换三,买的数量不变,但花的钱变少,唯一的条件知识可交换次数需要大于2(取消高指数幂的1次购买,换3次低指数幂的购买,净差2次交易次数),那么,代码如下,我们对着代码来进行分析

ci = k - ci;
if (ci < 0)
	cout << "-1" << endl;
else
{
	while (ci >= 2 && tou > 0)
	{
		if (ci - san[tou] * 2 >= 0)
		{
			ci -= san[tou] * 2;
			san[tou - 1] += 3 * san[tou];
			ans -= san[tou] * pow(3, tou - 1);
		}
		else
		{
			ci = ci / 2;
			ans -= ci * pow(3, tou - 1);
			ci = 0;
		}
		tou--;
	}
	cout << ans << endl;
}

        首先,将ci赋值为k-ci。算出剩余的可交易次数

        若交易次数比0 小,则输出-1即可,因为连最低交易次数都无法达到

        若交易次数非0,就进行操作

        经过我上面的讲述,需要将交易剩余次数要确保在2次以上,并且进制所在位不能在0位上(因为0位上的无法再进行退位,已经退到了末路),然后用一个循环,讲这俩个条件包括起来后,在循环内部再进行操作即可

        因为退一次位,需要2次交易次数,且,前一位的san会多3,所以我们可以用分支语句来进行操作,若剩余次数可以将当前位的全部退位,就进行正常操作,若不能全部退位,就只进行部分操作后结束即可,这就是上方代码中的分支语句作用。

        

        那么到这里,该题的核心就全部讲完了,接下来就是源码环节

        源代码

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <iostream>
#include <string.h>
#include <algorithm>
#include <math.h>
#include <queue>
using namespace std;

int t, n, k;
long long san[100];

int main()
{
	cin >> t;
	while (t--)
	{
		memset(san, 0, sizeof(san));
		int wei = 0;
		cin >> n >> k;
		for (wei = 0; pow(3, wei) <= n; wei++);
		wei--;
		int tou = wei;
		long long ci = 0;
		long long ans = 0;
		while (n)
		{
			int i = 0;
			for (i = 0; i <= 3; i++)
			{
				if (i * pow(3, wei) > n)
					break;
			}
			i--;
			ci += i;
			ans += i * (pow(3, wei + 1) + wei * pow(3, wei - 1));
			san[wei] = i;
			n -= i * pow(3, wei);
			wei--;
		}
		ci = k - ci;
		if (ci < 0)
			cout << "-1" << endl;
		else
		{
			while (ci >= 2 && tou > 0)
			{
				if (ci - san[tou] * 2 >= 0)
				{
					ci -= san[tou] * 2;
					san[tou - 1] += 3 * san[tou];
					ans -= san[tou] * pow(3, tou - 1);
				}
				else
				{
					ci = ci / 2;
					ans -= ci * pow(3, tou - 1);
					ci = 0;
				}
				tou--;
			}
			cout << ans << endl;
		}
	}
	return 0;
}

       

        那么源码讲完了,看着挺简单的,为什么我能WA这么多次呢,那么就进入错误分析环节,一开始我找自己错误时候是真的艰巨,后面的问题,倒是容易解决

        错误分析

        san数组重置的重要性

        我在一开始的时候,思路便以明确,但在代码执行的过程中,却出了点意外,这个意外是真的难找,那么,就让我们看看第一次我提交时的源码以及错误样例

        源代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <iostream>
#include <string.h>
#include <algorithm>
#include <math.h>
#include <queue>
using namespace std;

int t, n, k;
long long san[100];

int main()
{
	cin >> t;
	while (t--)
	{
		int wei = 0;
		cin >> n >> k;
		for (wei = 0; pow(3, wei) <= n; wei++);
		wei--;
		int tou = wei;
		long long ci = 0;
		long long ans = 0;
		while (n)
		{
			int i = 0;
			for (i = 0; i <= 3; i++)
			{
				if (i * pow(3, wei) > n)
					break;
			}
			i--;
			ci += i;
			ans += i * (pow(3, wei + 1) + wei * pow(3, wei - 1));
			san[wei] = i;
			n -= i * pow(3, wei);
			wei--;
		}
		ci = k - ci;
		if (ci < 0)
			cout << "-1" << endl;
		else
		{
			while (ci >= 2 && tou > 0)
			{
				while (san[tou] && ci >= 2)
				{
					ci -= 2;
					san[tou]--;
					san[tou - 1] += 3;
					ans -= pow(3, tou - 1);
				}
				tou--;
			}
			cout << ans << endl;
		}
	}
	return 0;
}
        错误样例

        因为错误在第二个样例中的第388个数据,所以想找也找不到,没招,我就开始研究

        思来想去,就只有pow会吞精度的可能导致判断出问题,于是我后面又进行了将pow强转long long,或者不用pow,使用循环达到pow的效果,但最终依旧WA在了这个地方,后面因为一直找不出问题,这题也停滞了一段时间

        为何这题数组重制很重要

        随后,我在今天看到时候,突然发现了在转三进制部分的while循环的代码的漏洞,若不对san数组进行重置,就会导致上一个的san数组中的数据是被保留下来的,但因while的判断条件是n,就导致若n为0后,循环就结束了,但若是n在不是最低位才为0,而是在前面就为0了,就会导致提前退出循环,没有将后面低位的数组中的数值清0,最终会导致计算初ans值的时候以及后续对位进行退位操作时数据变化的影响

        所以san数组重置是非常必要的

        注:但也不是一定要重置,也可以将while循环的判断条件改为wei>=0,这样就可以使数组全部初始化,只是这样内部的代码就也需要更改一下了

        超时问题

        在解决上面的重置问题后,我又交了一发,结果超时了,但这个问题就很简单了,就只需要对循环嵌套深的,且可以优化的部分进行优化即可,那么我们看下面源码进行分析

        源代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <iostream>
#include <string.h>
#include <algorithm>
#include <math.h>
#include <queue>
using namespace std;

int t, n, k;
long long san[1000];

int main()
{
	cin >> t;
	while (t--)
	{
		memset(san, 0, sizeof(san));
		int wei = 0;
		cin >> n >> k;
		for (wei = 0; pow(3, wei) <= n; wei++);
		wei--;
		int tou = wei;
		long long ci = 0;
		long long ans = 0;
		while (n)
		{
			int i = 0;
			for (i = 0; i <= 3; i++)
			{
				if (i * pow(3, wei) > n)
					break;
			}
			i--;
			ci += i;
			ans += i * (pow(3, wei + 1) + wei * pow(3, wei - 1));
			san[wei] = i;
			n -= i * pow(3, wei);
			wei--;
		}
		ci = k - ci;
		if (ci < 0)
			cout << "-1" << endl;
		else
		{
			while (ci >= 2 && tou > 0)
			{
				while (san[tou] && ci >= 2)
				{
					ci -= 2;
					san[tou]--;
					san[tou - 1] += 3;
					ans -= pow(3, tou - 1);
				}
				tou--;
			}
			cout << ans << endl;
		}
	}
	return 0;
}
        分析

        通过上面的代码与AC代码的比对,我们会发现,其实没什么区别,主要区别便是在这个俩层循环的优化,如图,这部分的优化就很容易懂了,在代码实现里也讲过了,就不过多叙述了

        


结语:

        那么,今日算法题的讲解就结束了,希望对你们有所帮助,谢谢观看,感觉不错可以分享给朋友一起看哟