该文章为方向三:实践记录以及工具使用
AI刷题优势之处
- AI刷题可以提高刷题效率,减少了很多花在代码框架上的时间,可以把时间集中在算法优化和调试bug中去,大幅度地节约了同学的时间,提高了同学们刷题的效率
- AI可以有效帮助你发现你代码里面存在的一些比较简单却难以发现的问题,减少了大量寻找小问题的时间,有效帮助你检查语法问题和变量名称错误
- AI可以帮你优化一些冗余的代码,手把手教你把一串冗长容易出错的代码转化为相同意思的清晰简单不易出错的代码,相比其他刷题只有ac与wa区别之外可以教会你如何简化代码和优化代码的可读性
- 遇到很难解决的问题,与其长时间泡在题目中感受无法解出的痛苦,使用AI虽然不能保证一定给出100%正确的答案,但是可以给你提供一个解题框架和解题思路,你可以跟随解题思路一步步自我思考和提示AI,直到解出正确答案
实践案例
选取自AI刷题集33.卡牌翻面求和问题,这是一道难度为难的题目
题目描述
小M有 nn 张卡牌,每张卡牌的正反面分别写着不同的数字,正面是 aiai,背面是 bibi。小M希望通过选择每张卡牌的一面,使得所有向上的数字之和可以被3整除。你需要告诉小M,一共有多少种不同的方案可以满足这个条件。由于可能的方案数量过大,结果需要对 107取模。
例如:如果有3张卡牌,正反面数字分别为
(1,2),(2,3)和(3,2),你需要找到所有满足这3张卡牌正面或背面朝上的数字之和可以被3整除的组合数。
解题经历
1.读完题,我就能想象到这道题的答案很有可能是个非常大的值,有非常多的情况,第一段最后一句话也确定了我的想法,平时我可能就会害怕地离开这道题,但是现在有AI的帮助,我就有自信面对这道题 2.根据题目要求对数字之和被3整除,可以翻译为模3为0,那我就会联想到对所有值都模3,对最后答案没有影响,但是数组中数据全部变为0,1,2中的一个,我认为可以大幅减少后续计算 3.接下来顺着题目我想到可以创建一个有序积数组来储存数据,表示成一叠卡片的样子,这样可以保证同一张卡片数字无法被复用。接下来我想到了如果采用1-0换另一张0-1或1-2,这样的不影响最终和替换方式来解题,然后发现这样做首先会造成多张卡组合的重复,以及无法判断同时3张加一转换同样成立的可能性
v_map=[]
for i in n:
a[i]%=3
b[i]%=3
v_map.append([a[i],b[i]])
4.然后我想到对于每个值都可以有个翻转差来求解。我们先把全部的v_map全部小的在前(这样为后面简化计算),然后加入一个参数max(a[i],b[i])-min(a[i]-b[i]),这样可以后期凑3来解题
if a[i]>b[i]:
v_map.append([a[i],b[i],a[i]-b[i]])
else:
v_map.append([b[i],a[i],b[i]-a[i]])
5.创建一个sum来计算卡片总和
sum=0
......
sum+=v_map[i][0]
if sum%3==0:
1
elif sum%3==1:
1
else:
1
6.找到结果为1或2的最大值和最小值,后面打算采用排列组合方法来解决,也就是尽可能凑成3来解题,因此写入阶乘函数,然后得到排列组合的结果。
def jc(n:int)->int:
if n==1 or n==0:
return 1
return n*jc(n-1)
min_mod=min(mod[1],mod[2])
max_mod=max(mod[1],mod[2])
result=0
if sum%3==0:
for count in range(0,min_mod+1,1):
result+=(jc(max_mod)//jc(count)//jc(max_mod-count)*jc(count))
#循环mod_min,c7iaii
这样我们就解决了如果牌全部取最小值之和可以模3的情况。 7.但是倘若最小值不能模3,我们还需要凑一个1或2,这个时候复杂度大量上升,头脑发热无法计算(),这个时候我有点放弃了,于是就要发挥AI刷题的特点,求出于AI。 8.我们可以先直接让AI给我们代码提示
但是他只给我了一些优化的算法(但是这里提示了我题目中的模运算,这是我代码过程中忽略的问题,这也是AI的优势所在,发现容易被忽略的问题),并没有告诉我如何解决其他两种情况的代码,这个时候就要发挥提示工程的魅力。(替换代码的的时候记得保留原代码的备份,毕竟AI代码不需要完全相信) 9.我已经告知了AI我做到哪里,并告诉AI他需要帮我做什么
但是他给的代码很不给力,无法运行同时很容易就能看出代码写的毫无意义,这里就不贴出展示,但是它提示了我们可以通过把卡片补齐3的办法来转化为第一种情况,于是自己再进行了思考 10.经过漫长的思考,我得出了第一个版本
def solution(n: int, a: list, b: list) -> int:
v_map = []
sum = 0
mod = [0] * 3
for i in range(n):
a[i] %= 3
b[i] %= 3
if a[i] < b[i]:
v_map.append([a[i], b[i], b[i] - a[i]])
else:
v_map.append([b[i], a[i], a[i] - b[i]])
mod[v_map[i][2]] += 1
sum += v_map[i][0]
min_mod = min(mod[1], mod[2])
max_mod = max(mod[1], mod[2])
result = 0
if sum % 3 == 0:
# 计算组合数
for count in range(min_mod + 1):
# 计算组合数 C(max_mod, count)
# 使用模运算避免溢出
result += (jc(max_mod) // jc(count) // jc(max_mod - count) * jc(count)) % (10**9 + 7)
elif sum % 3 == 1:
# 处理余数为1的情况
# 需要找到一种方法,使得总和模3变为0
# 可以通过选择某些卡片,使得它们的差值模3余2
# 计算组合数
result1=0
result2=0
if mod[1]<2 and mod[2]==0:
return 0
elif mod[2]>0:
min_mod = min(mod[1], mod[2]-1)
max_mod = max(mod[1], mod[2]-1)
for count in range(min_mod+1):
# 计算组合数 C(mod[1], count)
# 使用模运算避免溢出
result1 += (jc(max_mod) // jc(count) // jc(max_mod- count) * jc(count)) % (10**9 + 7)
result1=(result1-1)*mod[2]+1
else:
min_mod = min(mod[1]-2, mod[2])
max_mod = max(mod[1]-2, mod[2])
for count in range(min_mod+1):
# 计算组合数 C(mod[1], count)
# 使用模运算避免溢出
result2 += (jc(max_mod) // jc(count) // jc(max_mod- count) * jc(count)) % (10**9 + 7)
result2=(result2-1)*(jc(mod[1])/jc(2)/jc(mod[1]-2))+1
result=result1+result2
else:
# 处理余数为2的情况
# 需要找到一种方法,使得总和模3变为0
# 可以通过选择某些卡片,使得它们的差值模3余1
# 计算组合数
if mod[1]==0:
return 0
else:
min_mod = min(mod[1]-1, mod[2])
max_mod = max(mod[1]-1, mod[2])
for count in range(min_mod+1):
# 计算组合数 C(mod[1], count)
# 使用模运算避免溢出
result += (jc(max_mod) // jc(count) // jc(max_mod- count) * jc(count)) % (10**9 + 7)
result=(result-1)*mod[1]+1
# 最终结果需要对 10^9+7 取模
rev0=2**mod[0]
return (result * rev0) % (10**9 + 7)
# 优化后的阶乘函数
def jc(n: int) -> int:
if n == 1 or n == 0:
return 1
result = 1
for i in range(2, n + 1):
result = (result * i) % (10**9 + 7)
return result
if __name__ == '__main__':
print(solution(n=3, a=[1, 2, 3], b=[2, 3, 2]) == 3)
print(solution(n=4, a=[3, 1, 2, 4], b=[1, 2, 3, 1]) == 6)
运行并测试了多组数据之后发现问题,在处理剩余两种情况的时候会出现方案重复,且复杂难以运算的情况,我们只得再次求助于AI。
令人意外的是,这次AI并没有追随我的代码进行修改(毕竟复杂度稍微高了一些,需要比较多的排列组合来消除重复情况),而是根据消除重复方案的提示,给出了动态规划的解法,而他给出的代码刚好解出了这道题,代码也简洁清晰(这也变相意味着我前面写的大部分排列组合算法都没什么意义,但是这也是使用AI常发生的事情,可以提供更好更优质的解决方案,但也并不是说我之前做的无用,在最终版的代码上许多思路都是我原代码的思路,而在计算组合数的时候采用了动态规划来辅助解决)
最终代码
def solution(n: int, a: list, b: list) -> int:
MOD = 10**9 + 7
# 初始化dp数组
dp = [[[0 for _ in range(2)] for _ in range(3)] for _ in range(n + 1)]
dp[0][0][0] = 1 # 初始状态,前0张卡牌,总和模3余0,选择状态为0
for i in range(1, n + 1):
ai = a[i - 1] % 3
bi = b[i - 1] % 3
for j in range(3):
for k in range(2):
# 选择正面
dp[i][(j + ai) % 3][0] = (dp[i][(j + ai) % 3][0] + dp[i - 1][j][k]) % MOD
# 选择背面
dp[i][(j + bi) % 3][1] = (dp[i][(j + bi) % 3][1] + dp[i - 1][j][k]) % MOD
# 最终结果是前n张卡牌,总和模3余0的所有方案数之和
return (dp[n][0][0] + dp[n][0][1]) % MOD
if __name__ == '__main__':
print(solution(n=9, a=[3,4,17,2,11,14,4,3,11], b=[2,1,13,4,16,15,4,2,11]) == 168)
print(solution(n=4, a=[3, 1, 2, 4], b=[1, 2, 3, 1]) == 6)
思路就是对前n个卡片进行所有情况的模拟推演,并在第n+1张卡片计算时只需要本身值加上第n张卡片情况得出第n+1张卡片情况及所对应的值。为了更好理解,假设第n+1张卡片正面为4反面为1,那么计算中前n+1张卡片之和为1的情况就是前n张卡片之和为0的情况之和,为2就是为1情况之和,为0就是为2情况之和。这样我们就可以推出任意前n张卡片之和的所有情况,最后对所要求的模3余0只需要得到最后一张卡片对应模三余0的值就可以,也就是(dp[n][0][0] + dp[n][0][1]) % MOD
总结时刻
使用AI可以有效节约我们的时间,倘若没有AI,我很有可能在这道题会在我自己的方法消耗大量的时间最后却没有得出正确的答案甚至引发更多的问题。同时AI可以为你提供更多的思路,如果这道题没有AI提示,我并没有想起可以使用动态规划来解决问题。最后AI可以帮你解决一些细枝末节的问题,比如这题在使用阶乘和结果取模的地方都没有进行好的处理,很有可能引发递归次数过多或数据溢出的情况,但AI都帮你很好的注意到了。