[AGC009C Division into Two] [DP好题]

这题真的挺不错的。

首先观察发现:任意两元素相差>=A,只要满足排序后相邻两元素相差>=A,B同理,所以我们考虑从小到大依此决定每个元素应该扔到哪个集合,这样就只要维护两个集合中此时最大的元素即可。得到朴素的动态规划方法:

f[i][j]表示放完了前max(i,j)个元素,且A集合中最大的元素是i,B集合中最大的元素是j,有多少种方案。

转移也很方便,直接枚举下一个元素放到哪个集合即可,注意相邻元素的差值是否合法。

复杂度O(n^2)。

考虑优化。我们记录的信息太详细了,是否可以省掉一维呢?其实仔细想想,对于每个集合来说,我们真正想要知道的并不是最大元素是谁,而是当前元素能否放进去,所以我们考虑省掉其中一维信息(省得太多不好转移,所以只省掉一维比较方便),也就是用这样的新状态来表示:f[i]表示已经放完了前i个元素,第i个元素是在A集合,并且保证s[i+1]可以放进B集合,这时有多少方案;g[i]表示已经放完了前i个元素,第i个元素是在B集合,并且保证s[i+1]可以放进A集合,这时有多少方案。

这样我们的状态就变成线性的了。如何转移呢?

这里我们只讨论f[i]如何向后更新,因为g[i]是完全类似的。

往后更新只有两种情况,一种是将s[i+1]放入A集合:f[i]->f[i+1] (|s[i+1]-s[i]|>=A)

另一种则是将s[i+1]放入B集合,注意到这个时候我们会推到g[i+1],但是并不能保证s[i+2]可以放入A,所以我们应该找到最小的p(方法下面再说),满足s[p+1]可以放入A集合,然后将s[i+1..p]全部扔到B集合里面。 方程:f[i]->g[p]。

这样,我们就完成了O(N)的DP。上面每种方案显然合法,并且没有重复,所以(f[n]+g[n])<=正确答案,又由于每种正确方案都可以在上面的转移中体现出来,因此正确答案<=(f[n]+g[n]),因此(f[n]+g[n])=正确答案,这个DP是不重复、不遗漏的。

 

补充:如何求上述的p。我们用线性的方法预处理出四个数组,la,ra,lb,rb。

la[i]表示从i向后找到的第一个位置,满足| s[ la[i] ] - s[i] |>=A,ra[i]表示从i向后找到的第一个位置,满足| s[ ra[i]+1 ] - s[ ra[i] ] |

B数组同理。

显然p<=rb[i+1],因为超过这个位置后的元素就不能继续扔到B集合了,并且p>=la[i]-1,否则s[p+1]不能扔到A集合中。

因此如果rb[i+1]g[p]即可。

 

[总结]

    这题把握住了真正有用的信息,从而简化了状态,并且为了避免重复转移,选择了最近的一个合法位置来转移。这些思路都是很好的。

#include 
#include 
#define ll long long
#define rep(i,j,k) for (i=j;i<=k;i++)
#define down(i,j,k) for (i=j;i>=k;i--)
using namespace std;
const int N=1e5+5,INF=1e9,mod=1e9+7;
int n,i,j,ra[N],rb[N],la[N],lb[N];
ll A,B,s[N];
int f[N],g[N];
void updata(int &x,int y) { x+=y; if (x>=mod) x-=mod; }
int main()
{
	//freopen("division.in","r",stdin);
	//freopen("division.out","w",stdout);
	scanf("%d%lld%lld",&n,&A,&B);
	rep(i,1,n) scanf("%lld",&s[i]);
	s[n+1]=INF; ra[n]=rb[n]=ra[n+1]=rb[n+1]=n+1;
	down(i,n-1,1)
	{
		if (abs(s[i+1]-s[i])=A) updata(f[i+1],f[i]);
		if (rb[i+1]>=la[i]-1) updata(g[max(i+1,la[i]-1)],f[i]);
		if (abs(s[i+1]-s[i])>=B) updata(g[i+1],g[i]);
		if (ra[i+1]>=lb[i]-1) updata(f[max(i+1,lb[i]-1)],g[i]);
	}
	printf("%d\n",(f[n]+g[n])%mod);
	return 0;
}

 

你可能感兴趣的:(AGC好题(思维题))