1. 程式人生 > >[AGC003E Sequential operations on Sequence] [思路題:逆推與分治]

[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;
}