USACO 2020 JAN, GOLD - Problem B
ZJNU 2380 / ZJNU contest 1161B
题意
给定长度为N的数组,Q次询问,每次询问给定左右区间 a b
3SUM问题指对于一段区间,选定三个不同位置 i,j,k 使得 ai+aj+ak=0 成立,问这样的三元组组数
对于每次询问输出区间内这样的三元组组数
限制
1≤N≤5000
1≤Q≤105
1≤ai≤bi≤N
-106≤Ai≤106
解
仅为解法之一:基于区间长度的区间dp
令 dp[i][j] 表示所求的三元组 (a,b,c) 满足 i≤a,b,c≤j 的组数
区间长度从小到大进行规划,则可知大区间可以由比其小的区间规划而来
所以可以将 dp[i][j] 中的 i 和 j 看作是代表的答案的左右区间
首先得到相邻的 dp[i][j-1]+dp[i+1][j] ,即三元组均在左右区间为 [i,j-1] 以及 [i+1,j] 之内
发现这样计数时,中间的满足左右区间为 [i+1,j-1] 的三元组被重复计数,故需减去dp[i+1][j-1]
最后考虑到这样转移还需要加上表示当前状态的答案,即三元组中有两个固定为 i 和 j 时,满足题意的组数
既然固定了 i 和 j ,表示固定了其中两个数,第三个数 ak 可以由 ai+aj+ak=0 推得 ak=-(ai+aj)
引入cnt数组动态表示当前 [i+1.j-1] 内各数字出现的次数,则对于状态转移方程,能得到
dp[i][j]=dp[i][j-1]+dp[i+1][j]-dp[i+1][j-1]+cnt[-a[i]-a[j]];
因为数组索引需要为非负数,又考虑到数据范围为 -106≤Ai≤106
所以可以将读入的数全部加上 ave=106 (稍大一点),使其全部成为非负数,再用 cnt[0~2000000] 来表达
每个数都加上基准后,ai+aj+ak=ave*3 ,故第三个数字 ak=ave*3-(ai+aj)
并且注意在使用状态转移方程时判断 ak 是否会越界(致RE)
三元组最小长度为3,故从3开始枚举长度
首先,将 [l+1,r-1] 的数加入cnt数组中
每次枚举,让左边界从1开始,右边界则从len开始,对于cnt即对应 [2,len-1]
每次左边界与右边界需要往右移动一格(窗口整体右移)
所以原本 A[i+1] 的位置会变成左边界,使其从cnt数组中减去
原本 A[j] 的位置会从此时的右边界变成界内元素,故将其加入cnt数组中
整段处理结束后(右边界越界时),需要将cnt数组清零,但直接memset或者遍历清零复杂度很高
所以考虑到最后一次的左右边界分别为 n-len+1 以及 n
所以此时 cnt 数组表示的范围为 [n-len+2,n-1]
又因为转移而使得表示范围变成 [n-len+3,n] ,所以将这一段遍历清零即可
处理完dp数组,对于每个询问 l 与 r,输出 dp[l][r] 即可
完整程序
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int ave=1000025,ave3=3000075; //读入数所需要加的基准
int A[5050];
ll dp[5050][5050];
int cnt[2000050];
void solve()
{
int n,q,l,r,tmp;
cin>>n>>q;
for(int i=1;i<=n;i++)
cin>>A[i],A[i]+=ave;
for(int len=3;len<=n;len++)
{
for(int i=2;i<len;i++)
cnt[A[i]]++;
for(int i=1,j=len;j<=n;i++,j++)
{
tmp=ave3-(A[i]+A[j]);
if(tmp>=0&&tmp<2000050)
dp[i][j]=dp[i][j-1]+dp[i+1][j]-dp[i+1][j-1]+cnt[tmp];
else
dp[i][j]=dp[i][j-1]+dp[i+1][j]-dp[i+1][j-1];
cnt[A[j]]++;
cnt[A[i+1]]--;
}
for(int i=n-len+3;i<=n;i++)
cnt[A[i]]--;
}
while(q--)
{
cin>>l>>r;
cout<<dp[l][r]<<'
';
}
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
solve();
return 0;
}