题目:
给出一个长度为n的序列,问有多少种方案将序列划分为恰好连续的三段(每个元素都属于某一段),使得每一段的和都相等。
格式
输入格式:
第一行:一个整数n;
第二行:n个整数,表示序列。
输出格式:
一个整数表示方案数。
样例 1
输入:
4
1 2 3 3
复制
输出:
1
TLE代码:双层循环
cpp
#include<bits/stdc++.h>
using namespace std;
long long sum[100005];
long long a[100005];
int main( )
{
long long n;
cin>>n;
sum[0]=0;
for(int i=1;i<=n;i++){
cin>>a[i];
sum[i]=sum[i-1]+a[i];
}
long long ans=0;
for(int i=1;i<=n-2;i++){
for(int j=i+1;j<=n-1;j++){
if((sum[i]==sum[j]-sum[i])&&(sum[i]==sum[n]-sum[j]))
ans++;
}
}
cout<<ans;
return 0;
}
优化:
s1=sum[i],s2=sum[j]-sum[i] s3 =sum[n]-sum[j]
1.第一个浪费点------既然 s1=s2,那 sum [j] 等于什么?
sum[j] 是前 j 个元素的和,也就是第一段加第二段,即sum[j] = s1 + s2
现在你的 if 里已经要求 s1 == s2 了,那我们把 s2 换成 s1,代入上面的式子:
sum[j] = s1 +s2= 2 * s1
2.第二个浪费点------那 s1 又必须等于多少呢?
我们再看整个数组的总和 total:
total = s1 + s2 + s3
你的 if 要求 s1 = s2 = s3,那总和就是:
total = s1 + s1 + s1 = 3 *s1
反过来算,s1 就必须等于:
s1 = total / 3
我们给这个固定值起个名字,就叫 target(目标值)。target=total / 3
3.把前两步的结论拼回去 ------ if 条件完全变样了
cpp
if (sum[i] == target && sum[j] == 2*target)
4.数有多少对 (i,j)
举个例子
total = 6,所以 target = 2,2*target = 4
我们算出来的 sum 数组是:[0, 1, 2, 3, 4, 5, 6]
我把这个 sum 数组写成一排,像这样:
cpp
下标: 0 1 2 3 4 5 6
sum: [0] [1] [2] [3] [4] [5] [6]
↑ ↑
target 2*target
现在我问你:要数有多少对 (i,j) 满足 i<j,sum [i]=2,sum [j]=4,你会怎么数?
你肯定会这样数(这是人的本能,根本不是算法):
-
你从左往右走;
-
走到下标 2,看到 sum[2]=2,你就在心里记一笔:"现在我遇到了 1 个 target";
-
继续走到下标 4,看到 sum[4]=4,你就想:"刚才我记了 1 个 target,那这里就有 1 种配对方案",然后把答案加 1;
-
数完了。
你看! 你根本不会用两层循环去 "枚举所有 i 和 j",你只会 **"一边走,一边记,遇到目标就加"**!
cpp
long long cnt = 0; // 记一下遇到了多少个 target
long long ans = 0;
for (int j = 1; j <= n-1; j++) {
// 先看:现在是不是 2*target?如果是,答案加上之前记的 cnt
if (sum[j] == 2 * target) {
ans += cnt;
}
// 再看:现在是不是 target?如果是,记下来 cnt++
if (sum[j] == target) {
cnt++;
}
}
总结:
1.把你的 if 条件抄在草稿纸上;
-
用简单的代数移项,把条件翻译成「sum [j] 等于什么」、「sum [i] 等于什么」;
-
代入一个超级简单的小例子,用手「从左到右数一遍」,看看你自己是怎么数的。
积累反射:
| 题目信号 | 条件反射想到的方法 | 原因 |
|---|---|---|
| 求「连续子段和」 | 前缀和数组 | 前缀和就是专门用来快速算连续子段和的 |
| 数据规模 n≤1e5 | 必须用 O(n) 或 O(nlogn) 的算法 | O(n2) 的算法在 n=1e5 时,运算次数是 1e10,电脑 1 秒只能跑约 1e8 次,肯定超时 |
| 求「满足条件的配对数 (i,j)」且 i<j | 一边遍历一边计数,不用枚举所有配对 | 只要统计「前面出现过多少次目标值」,遇到当前值时直接加次数即可 |
| 「分成 k 段,每段和相等」 | 先算总和,判断是否能被 k 整除 | 总和必须是 k 的倍数,否则直接无解,这是数学上的必要条件 |
数学直觉的培养
------ 先写公式,再写代码
盯着你的暴力代码问:
-
「哪一步计算是重复的?」(比如重复算子段和→前缀和)
-
「哪一层循环是多余的?」(比如枚举所有 i→只需要计数)
-
「有没有数学上的必要条件,可以先排除无解的情况?」(比如总和 %3==0)
-
第三阶段:「看题解,补思维漏洞」
如果你实在想不出优化方法,就去看题解,但不要只抄代码,要问自己:
-
「题解的哪一步是我没想到的?」
-
「我为什么没想到?是因为不知道某个数据结构,还是因为没发现某个数学规律?」
-
「把这个题的『信号 - 方法』对应关系,记到我的笔记本上。」
-
第四阶段:「换个问法,举一反三」
做完这个题,你可以自己给自己出题:
-
「如果是分成 4 段,每段和相等,怎么做?」
-
「如果是求『有多少个连续子段和等于 target』,怎么做?」(这就是经典的「前缀和 + 哈希表」题)
-
「如果数组里有负数,这个方法还能用吗?」(当然能用,前缀和天然支持负数
正确代码:
cpp
#include<bits/stdc++.h>
using namespace std;
int main( )
{
long long n;
cin>>n;
long long a[n],sum=0;
for(int i=0;i<n;i++){
cin>>a[i];
sum+=a[i];
}
if(sum%3!=0){
cout<<0;
return 0;
}
long long count=0,sum0=0;
long long target=sum/3;
long long ans=0;
for(int i=0;i<n-1;i++){
sum0+=a[i];
if(sum0==target*2) ans+=count;
if(sum0==target) count++;
}
cout<<ans;
return 0;
}