【 题 意 】 : \color{blue}{【题意】:} 【题意】: 给你一组数(个数为 n n n),问是否可以最多移动一个数,使得这一串数可以分成两个部分,每一部分所有数的和相等。如果可以,则输出YES
,否则请输出NO
。(翻译摘自洛谷)
【 思 路 】 : \color{blue}{【思路】:} 【思路】: 首先,最基本的,如果这组数的总和(记为 s u m sum sum)是奇数,那么肯定输出NO
。
在接着,我们可以考虑暴力,即枚举把哪个数移动到哪个位置上,但是会超时。
我们自然而然可以想到这样的算法:从前往后枚举 i i i,统计 ∑ j = 1 i a j \sum^{i}_{j=1} a_j ∑j=1iaj(我们记为 a n s ans ans),如果 s u m 2 − a n s \frac{sum}{2}-ans 2sum−ans在原数组出现过,那么就可以达到题目要求。
#define gc getchar()
#define g(c) isdigit(c)
inline int read(){
char c=0;int x=0;bool f=0;
while (!g(c)) f=c=='-',c=gc;
while (g(c)) x=x*10+c-48,c=gc;
return f?-x:x;
}
const int N=1e5+100;
typedef long long ll;
int a[N],n,b[N];ll ans,sum;
bool binary_search(int p){
int l=1,r=n,mid;
while (l<=r){
mid=(l+r)>>1;
if (b[mid]==p)
return true;//找到数字p了,返回
else if (b[mid]>p)
r=mid-1;
else l=mid+1;
}
return false;//找到这了,证明没有这个数字
}
//函数功能:查找b(排序后的a数组)中是否有数字p
int main(){
n=read();ans=sum=0;
for(int i=1;i<=n;i++){
a[i]=b[i]=read();
sum=sum+a[i];
}
if (sum%2){
printf("NO");
return 0;
}
sort(b+1,b+n+1);
for(int i=1;i<=n;i++){
ans+=a[i];
if (binary_search(sum/2-ans)){
printf("YES");
return 0;
}
}
printf("NO");
return 0;
}
我们发现这样做是错的 (惨痛的教训),原因有两个:一是我们只考虑了把后面的数字往前移动,其实际上,后面的数字也可以往前移动。解决方案是在加一重枚举,从后往前考虑。二是如果我们需要的数字只有一个,而且在 [ 1 , i ] [1,i] [1,i]之中,我们就会把它当作两个。解决方案是只在 [ i + 1 , n ] [i+1,n] [i+1,n]中查找它是否存在。
#define gc getchar()
#define g(c) isdigit(c)
inline int read(){
char c=0;int x=0;bool f=0;
while (!g(c)) f=c=='-',c=gc;
while (g(c)) x=x*10+c-48,c=gc;
return f?-x:x;
}
const int N=1e5+100;
typedef long long ll;
int a[N],n,b[N];ll ans,sum;
bool find(ll p,int begin,int end){
if (p==0) return true;
int l=begin,r=end,mid;
while (l<=r){
mid=(l+r)>>1;
if (b[mid]==p)
return true;//找到数字p了,返回
else if (b[mid]>p)
r=mid-1;
else l=mid+1;
}
return false;//找到这了,证明没有这个数字
}
//函数功能:查找b(排序后的a数组)从begin到end中是否有数字p
int main(){
n=read();ans=sum=0;
for(int i=1;i<=n;i++){
a[i]=b[i]=read();
sum=sum+a[i];
}
if (sum%2){
printf("NO");
return 0;
}
sort(b+1,b+n+1);
for(int i=1;i<=n;i++){
ans+=a[i];if (ans>sum/2) break;
if (find(sum/2-ans,i+1,n)){
printf("YES");
return 0;
}
}
ans=0;
for(int i=n;i;i--){
ans+=a[i];if (ans>sum/2) break;
if (find(sum/2-ans,1,i-1)){
printf("YES");
return 0;
}
}
printf("NO");
return 0;
}
虽然这样做看上去是完美无瑕的,然而我们发现这样还是错的 (又是惨痛的教训)。为什么呢(请读者稍作停留,思考一下,这对提升你的思维能力很有帮助!)?
答案是这样的。我们把 a a a数组排序后得到了 b b b数组,但是由于排序会 破 坏 原 数 组 前 后 数 字 间 的 相 对 位 置 \color{red}{破坏原数组前后数字间的相对位置} 破坏原数组前后数字间的相对位置。所以 b b b数组中的第 1 1 1到 i − 1 i-1 i−1个数也好,第 i + 1 i+1 i+1到第 n n n个数也罢,都不一定是 a a a数组中的原数,也就是说,我们依然没有解决如上所述的问题!!!
解决方法有两个:一是每次把 a a a数组中需要查找的数放入 b b b数组中,但是这样做可能会导致超时。二是运用前缀和算法。把 b b b数组改为 a a a数组的前缀和数组。在第一重循环中,我们查找 b [ i + 1.. n ] b[i+1..n] b[i+1..n]中是否有 b [ n ] 2 + a [ i ] \frac{b[n]}{2}+a[i] 2b[n]+a[i]( b [ n ] b[n] b[n]为总和),如果有,则把 a [ i ] a[i] a[i]移动到这个位置之后,正好 O K OK OK;第二重循环类似,只是查找 b [ n ] 2 − a [ i ] \frac{b[n]}{2}-a[i] 2b[n]−a[i],请读者思考为什么。
#define gc getchar()
#define g(c) isdigit(c)
inline int read(){
char c=0;int x=0;bool f=0;
while (!g(c)) f=c=='-',c=gc;
while (g(c)) x=x*10+c-48,c=gc;
return f?-x:x;
}
const int N=1e5+100;
typedef long long ll;
int a[N],n;ll b[N];
bool find(ll p,int begin,int end){
int l=begin,r=end,mid;
while (l<=r){
mid=(l+r)>>1;
if (b[mid]==p)
return true;//找到数字p了,返回
else if (b[mid]>p)
r=mid-1;
else l=mid+1;
}
return false;//找到这了,证明没有数字p
}
//函数功能:查找b(a数组的前缀和数组)从begin到end中是否有数字p
int main(){
freopen("t1.in","r",stdin);
n=read();
for(int i=1;i<=n;i++){
a[i]=read();
b[i]=b[i-1]+a[i];
}
//因为a数组内的数都是正整数,所以b数组天然有序
if (b[n]%2){//总和是奇数,一定不能分为两个相等的部分
printf("NO");
return 0;
}
for(int i=1;i<=n;i++)
if (find(b[n]/2+a[i],i+1,n)){
printf("YES");
return 0;
}
for(int i=n;i;i--)
if (find(b[n]/2-a[i],1,i-1)){
printf("YES");
return 0;
}
printf("NO");
return 0;
}
终于 A C AC AC了。