1. 程式人生 > 實用技巧 >【洛谷7116】微信步數(拉格朗日插值)

【洛谷7116】微信步數(拉格朗日插值)

點此看題面

  • 有一個\(k\)維場地,第\(i\)維範圍為\([1,w_i]\)
  • \(n\)個操作,每個操作可以表示為將某一維的座標\(±1\)
  • 你會先選定一個起點,然後不斷重複依次執行這\(n\)個操作直至走出場地。
  • 場地中每個位置都會被作為一次起點,你想要知道從所有點出發執行操作次數的總和。
  • \(n\le5\times10^5,k\le10\)

真的想不到這次\(NOIP\)唯一一道做出來的題目居然是\(T4\)。。。

暴力思路

考慮對於每一個操作\(i\),我們統計以它為最後一個操作的總貢獻。

首先一個最基本的結論,各維之間的位移是獨立的,因此我們的物件就是這維最先越界的點

既然是暴力,那麼我們就去列舉是在第\(x+1\)

輪結束的,但發現第\(1\)輪的情況和之後的情況可能略有一些區別,需要單獨討論。

這裡先設\(sum_t,smx_t,smn_t\)分別表示執行到第\(i\)個操作時第\(t\)維的位移、歷史最大位置、歷史最小位置。

然後類似地設\(tot_t,Mx_t,Mn_t\)分別表示執行完一輪操作之後第\(t\)維的位移、歷史最大位移、歷史最小位置。

方便起見,我們強制所有\(tot_t\ge0\)。這隻要在\(tot_t<0\)時把所有第\(t\)維的操作都取反就可以了。

在第\(1\)輪結束

首先考慮它是否可能作為一個結束操作。

那麼先要滿足不存在某一維\(t\)使得\(smx_t-smn_t\ge w_t\)

,因為這就說明所有點都必然已經走出了。

否則,還需要滿足當前的\(sum_{c_i}\)不在\([smn_{c_i},smx_{c_i}]\)範圍內,因為在這範圍內意味著這個位移之前已經達到過,就不可能在這一次操作越界。

如果這兩個條件都滿足了,就意味著這個操作可以作為結束操作,且因這個操作越界的點的第\(c_i\)維的座標只有一種取值。

接著考慮其他維度,對於第\(t\)維,已經有\(smx_t-smn_t\)個點越界了,那麼還有\(w_t-(smx_t-smn_t)\)種取值。

所以方案數就是:

\[\sum_{t\not=c_i}(w_t-(smx_t-smn_t)) \]

在第\(x+1\)
輪結束

這裡先講一下第\(x+1\)輪時每一維位移實際上的歷史最大值和歷史最小值。

由於\(tot_t\)已經強制大於等於\(0\)了,那麼歷史最小值實際上就是\(Mn_t\)

而歷史最大值既可能是上一輪的歷史最大值,也可能是這一輪新的歷史最大值,也就是\(\max\{tot_t\times(x-1)+Mx_t,tot_t\times x+smx_t\}\),也可以寫作\(tot_t\times x+\max\{Mx_t-tot_t,smx_t\}\)

接著,我們依舊先考慮它是否可能作為一個結束操作。

第一個必要條件依舊是此時每一維依然都滿足未全部越界,即每一維的歷史最大值減歷史最小值都不超過這一維的座標範圍。

第二個條件就是滿足當前達到的位置之前從未到達過,需要滿足\(tot_{c_i}\not=0,sum_{c_i}>smx_{c_i},Mx_t-tot_t<smx_{c_i}\),應該都是比較好理解的,這裡就不多做解釋了。

而此時的答案就是:

\[(nx+i)\sum_{t\not=c_i}(w_t-(tot_t\times x+\max\{Mx_t-tot_t,smx_t\}-Mn_t)) \]

暴拆多項式

首先在第\(1\)輪結束的貢獻我們是可以直接做的,那麼關鍵就是後面的部分了。

根據我們推出的答案式,發現其中很多項實際上都是常量,它其實就是\(k\)個關於\(x\)的二項式乘在了一起!

對於每一個操作我們直接暴力多項式乘法把這個多項式拆開,同時發現最多操作輪數\(p\)也是可以直接列舉統計的。

因此我們要做的就是求\(\sum_{x=1}^pf(x)\)

考慮把它按不同的冪次分別考慮,其實這就是\(k\)個自然數冪和!

那麼直接拉格朗日插值與處理一下自然數冪和的係數就可以做了。

程式碼:\(O(nk^2)\)

//考場程式碼,可能有點醜陋
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 500000
#define K 10
#define X 1000000007
using namespace std;
const int f[11][15]=//打完表之後才想起拉格朗日插值的打表程式碼可以直接放程式碼裡,但打都打完就算了
{
	{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
	{0,500000004,500000004,0,0,0,0,0,0,0,0,0,0,0,0},
	{0,166666668,500000004,333333336,0,0,0,0,0,0,0,0,0,0,0},
	{0,0,250000002,500000004,250000002,0,0,0,0,0,0,0,0,0,0},
	{0,766666672,0,333333336,500000004,400000003,0,0,0,0,0,0,0,0,0},
	{0,0,916666673,0,416666670,500000004,166666668,0,0,0,0,0,0,0,0},
	{0,23809524,0,833333339,0,500000004,500000004,142857144,0,0,0,0,0,0,0},
	{0,0,83333334,0,708333338,0,583333338,500000004,125000001,0,0,0,0,0,0},
	{0,766666672,0,222222224,0,733333338,0,666666672,500000004,111111112,0,0,0,0,0},
	{0,0,450000003,0,500000004,0,100000000,0,750000006,500000004,700000005,0,0,0,0},
	{0,348484851,0,500000003,0,1,0,1000000006,0,833333340,500000004,818181824,0,0,0}
};
int n,k,c[N+5],d[N+5],w[K+5];
int tot[K+5],Mx[K+5],Mn[K+5],sum[K+5],smx[K+5],smn[K+5],g[K+5];
I void Solve()
{
	RI i,j,p;for(i=1;i<=n;++i) tot[c[i]]+=d[i],//模擬一輪操作統計tot,Mx,Mn
		Mx[c[i]]=max(Mx[c[i]],tot[c[i]]),Mn[c[i]]=min(Mn[c[i]],tot[c[i]]);
	for(i=1;i<=k;++i) if(tot[i]) break;if(i>k)
		for(i=1;i<=n;++i) if(Mx[i]-Mn[i]<w[i]) puts("-1"),exit(0);//判死迴圈
	for(i=1;i<=k;++i) if(tot[i]<0) for(tot[i]*=-1,//強制tot≥0
		Mx[i]*=-1,Mn[i]*=-1,swap(Mx[i],Mn[i]),j=1;j<=n;++j) if(c[j]==i) d[j]*=-1;
	RI s,t=0,ans=0,op,o,a,b;for(i=1;i<=n;++i)//列舉每個操作
	{
		if((sum[c[i]]+=d[i])>=smn[c[i]]&&sum[c[i]]<=smx[c[i]]) continue;//如果不可能產生貢獻就跳過
		if(sum[c[i]]>smx[c[i]]) ++smx[c[i]],op=1;else --smn[c[i]],op=0;//更新歷史最值,op記下是更新哪種最值,後面會用到
		if(smx[c[i]]-smn[c[i]]==w[c[i]])//如果這一個操作直接走完了
		{
			for(p=1,j=1;j<=k;++j) p=1LL*p*w[j]%X;ans=(1LL*(p-t+X)*i+ans)%X;goto Print;//用總點數減去已有點數,求出剩餘總答案
		}
		for(s=j=1;j<=k;++j) if(j^c[i]) s=1LL*s*(w[j]-(smx[j]-smn[j]))%X;//第1輪結束
		t=(t+s)%X,ans=(1LL*i*s+ans)%X;//統計答案
		if(!tot[c[i]]||!op||Mx[c[i]]-tot[c[i]]>=smx[c[i]]) continue;//如果不可能在第x+1輪結束就跳過
		for(j=1;j<=k;++j) g[j]=0;g[0]=1;//清空多項式
		for(p=(w[c[i]]-(smx[c[i]]-Mn[c[i]]))/tot[c[i]],j=1;j<=k;++j) if(j^c[i])
		{
			a=tot[j],b=max(smx[j],Mx[j]-tot[j])-Mn[j];
			if(!a) {if(b>=w[j]) {p=0;break;}}else p=min(p,(w[j]-1-b)/a);//p統計最多進行輪數
			a=(X-a)%X,b=(w[j]-b+X)%X;
			for(s=k;~s;--s) g[s]=(1LL*b*g[s]+(s?1LL*a*g[s-1]:0))%X;//將這個二項式ax+b乘到總多項式上
		}
		if(p<=0) continue;//如果進行輪數小於等於0跳過
		for(s=1LL*g[0]*p%X,j=1;j<=10;s=(1LL*g[j]*a+s)%X,++j)
			for(b=1,a=o=0;o<=14;++o) a=(1LL*f[j][o]*b+a)%X,b=1LL*b*p%X;t=(t+s)%X;//統計點數
		for(s=k;~s;--s) g[s]=(1LL*i*g[s]+(s?1LL*n*g[s-1]:0))%X;//乘上二項式nx+i
		for(s=1LL*g[0]*p%X,j=1;j<=10;s=(1LL*g[j]*a+s)%X,++j)
			for(b=1,a=o=0;o<=14;++o) a=(1LL*f[j][o]*b+a)%X,b=1LL*b*p%X;ans=(ans+s)%X;//統計答案
	}
	Print:printf("%d\n",ans);
}
int main()
{
	freopen("walk.in","r",stdin),freopen("walk.out","w",stdout);
	RI i;for(scanf("%d%d",&n,&k),i=1;i<=k;++i) scanf("%d",w+i);
	for(i=1;i<=n;++i) scanf("%d%d",c+i,d+i);return Solve(),0;
}