[AGC003E Sequential operations on Sequence] [思路题:逆推与分治]

[题目大意]

    给出一个长度为N的序列A,其中A[i]=i。然后对它依次进行M次操作:每一次操作用一个整数q[i]描述,表示构造一个无穷长的序列B=AAAAAA...,然后令A=B[1..q[i]]。M次操作全部结束后,对每一个i∈[1,N]询问:A中有多少项等于i。

[思路]

这题有毒!!(话说AGC系列哪个没毒??)

首先画几个序列观察一下吧:

n=3,m=2,q={8,21}

初始:A0=(1,2,3)

第一次操作后:A1=[(1,2,3),(1,2,3),1,2]

第二次操作后:A2={ [(1,2,3),(1,2,3),1,2] , [(1,2,3),(1,2,3),1,2] , (1,2,3),1,2}

我们发现,最终的序列Am满足一种"可拆"的优美性质:Am其实可以由[之前的Ai]+[长度不足N的A0的前缀]这两类序列拼接而成,比如上面A2=A0+A0+(1,2)+A0+A0+(1,2)+A0+(1,2),也可以这样写:A2=A1+A1+A0+(1,2)。

这启发我们倒过来,把Am拆分成更小的Ai,分治地解决问题。(先用单调栈把所有q预处理一下,去掉那些没有后面长的q(显然不影响答案),这样q就单调递增了,不会出现细节问题)

我们起初拥有一个序列Am,考虑令i从大到小,每一次将现在拥有的num[j]个Ai全部拆分掉:

二分找到最长的Aj,使得|Aj|<|Ai|,然后将Ai拆成Aj+Aj+...+Aj+B。

前面的一堆Aj都可以先不管,等到拆Aj的时候再处理。

可以发现这个B仍然满足“可拆”的优美性质,我们继续用上面那种方法去拆它,直到拆完,或者无法再拆得更小(|B|<=N)。这个时候,剩下的B就是A0的前缀,我们现在就把它的贡献处理掉:直接将计数器cnt[1..|B|]全部加num[i]即可。

这样从大到小,每一次将Ai拆成更小的A的组合,全部搞好后,原来的Am被拆成了若干A0以及A0的前缀,这些无法拆分的"零头"都已经在拆的过程中计入了答案。

最后直接输出计数器cnt[1..N]即为答案。

[复杂度]

整个算法过程由m次拆分组成,每一次拆分显然不会超过log(q[i])次,每一次用二分来计算。所以总复杂度O(mlogQlogm),其中Q是max{q[i]}。可以通过10^5的数据。

[总结]

   做这类题目,用不到多少高端的算法,但要很快想到正解真的好困难。。只有仔细观察,永不言弃,多角度灵活尝试,才可能搞出正解!

Code:

#include <cstdio>
#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;
ll n,m,top,i,div,rest,q[N],num[N],s[N],stk[N];
ll erfen(ll x)
{
	ll l=0,r=m,mid;
	while (l<r)
	{
		if (r-l>1) mid=(l+r)>>1;
		else mid=r;
		if (q[mid]<x) l=mid;
		else r=mid-1;
	}
	return l;
}
int main()
{
	scanf("%lld%lld",&n,&m); q[1]=n;
	rep(i,2,m+1) scanf("%lld",&q[i]);
	rep(i,1,m+1)
	{
		while (q[i]<=stk[top]) top--;
		stk[++top]=q[i];
	}
	m=top;
	rep(i,1,m) q[i]=stk[i];
	num[m]=1;
	down(i,m,1)
	{
		if (!num[i]) continue;
		rest=q[i];
		while (rest>0)
		{
			div=erfen(rest);
			if (!div) break;
			num[div]+=rest/q[div]*num[i];
			rest%=q[div];
		}
		s[1]+=num[i]; s[rest+1]-=num[i];
	}
	rep(i,1,n) { s[i]+=s[i-1]; printf("%lld\n",s[i]); }
	return 0;
}

猜你喜欢

转载自blog.csdn.net/Orzage/article/details/84402963