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