1. 程式人生 > 實用技巧 >【AtCoder】AtCoder Grand Contest 015 解題報告

【AtCoder】AtCoder Grand Contest 015 解題報告

點此進入比賽

真沒想到居然能自己不看題解做完一場AGC。

不過這應該是一場比較老的比賽了,感覺相對最近的AGC來說題目難度確實算是偏低的了。

A:A+...+B Problem(點此看題面

  • 有一個長度為\(n\)的序列,已知其最小值和最大值。
  • 求序列可能的和有多少種。
  • \(n\le10^9\)

簽到題

顯然確定一個最小值和一個最大值之後,其他位置可以在\([A,B]\)中任選,因此共有\((n-2)\times(B-A)+1\)種可能的和。

但要注意\(A>B\)\(n=1,A\not=B\)兩種答案為\(0\)的情況。

程式碼:\(O(1)\)

#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
using namespace std;
int n,A,B;
int main()
{
	if(scanf("%d%d%d",&n,&A,&B),A>B||(n==1&&A^B)) return puts("0"),0;//判無解
	return printf("%lld\n",1LL*(n-2)*(B-A)+1),0;//直接計算答案
}

B:Evilator(點此看題面

  • 一個\(n\)層的樓,從第\(i\)層出發的電梯可以向給定方向\(s_i\)UD)一次執行任意層。
  • \(f(i,j)\)表示從第\(i\)層到第\(j\)層至少需要坐幾次電梯,求\(\sum_{i=1}^n\sum_{j=1}^nf(i,j)\)
  • \(n\le10^5\),保證\(s_1\)U\(s_n\)D

簽到題

顯然,從第\(i\)層樓往上到任意層,如果\(s_i\)U則需要一次,為D則需要兩次。往下的情況與之相反。

那麼只要列舉從哪層樓出發直接做即可。

程式碼:\(O(n)\)

#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 100000
using namespace std;
int n;char s[N+5];
int main()
{
	scanf("%s",s+1),n=strlen(s+1);
	RI i;long long t=0;for(i=1;i<=n;++i) t+=n-1+(s[i]^'U'?n-i:i-1);//列舉從第i層樓出發
	return printf("%lld\n",t),0;
}

C:Nuske vs Phantom Thnook(點此看題面

  • 給定一個\(n\times m\)\(01\)矩陣,滿足所有\(1\)構成的連通塊是森林。
  • \(q\)組詢問,每次求一個子矩陣中\(1\)構成的連通塊數。
  • \(n,m\le2\times10^3,q\le2\times10^5\)

套路題

眾所周知,森林滿足一個性質:連通塊數=點數-邊數。

那麼我們只要知道詢問的子矩陣中的點數和邊數,就可以計算出連通塊數了。

而這隻要二維字首和差分一下就行了。

程式碼:\(O(nm+q)\)

#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 2000
using namespace std;
int n,m,Qt,d[N+5][N+5],h[N+5][N+5],l[N+5][N+5];char a[N+5][N+5];
int main()
{
	RI i,j;for(scanf("%d%d%d",&n,&m,&Qt),i=1;i<=n;++i) scanf("%s",a[i]+1);
	for(i=1;i<=n;++i) for(j=1;j<=m;++j)
		d[i][j]=d[i-1][j]+d[i][j-1]-d[i-1][j-1]+(a[i][j]&1),//點
		h[i][j]=h[i-1][j]+h[i][j-1]-h[i-1][j-1]+(a[i][j]&a[i-1][j]&1),//行之間的邊
		l[i][j]=l[i-1][j]+l[i][j-1]-l[i-1][j-1]+(a[i][j]&a[i][j-1]&1);//列之間的邊
	RI xx,yx,xy,yy,t;W(Qt--) scanf("%d%d%d%d",&xx,&yx,&xy,&yy),
		t=d[xy][yy]-d[xx-1][yy]-d[xy][yx-1]+d[xx-1][yx-1],
		t-=h[xy][yy]-h[xx][yy]-h[xy][yx-1]+h[xx][yx-1],
		t-=l[xy][yy]-l[xx-1][yy]-l[xy][yx]+l[xx-1][yx],printf("%d\n",t);return 0;//連通塊數=點數-邊數
}

D:A or...or B Problem(點此看題面

  • 給定\(A,B\),你可以在\([A,B]\)中選任意個數按位或起來,求可能的結果數。
  • \(A\le B<2^{60}\)

初步思路

首先,若\(A=B\),顯然答案就是\(1\)

否則,發現\(A\)\(B\)中相同的字首無論如何都不可能改變,我們索性把它們全刪掉。

現在\(A\)\(B\)的最高位(設其為第\(i\)位)是不同的,且顯然\(A\)的最高位為\(0\)\(B\)的最高位為\(1\)

然後我們考慮把可能的結果分成兩類:第\(i\)位為\(0\)和第\(i\)位為\(1\)

\(i\)位為\(0\)

此時就相當於\([2^i,B]\)中的數都不能選擇, 那麼就是\([A,2^i)\)中選擇的方案數

然後,\([A,2^i)\)顯然都是可以作為答案的,而若在此基礎上或上任何一個數,都不可能使值變小,也不可能使值超出\(2^i-1\),因此\(2^i-A\)就是這種情況下的答案

\(i\)位為\(1\)

方便起見,先除去\(B\)中的第\(i\),就變成在\([A,2^i)\)\([0,B]\)中選擇。

然後考慮找到\(B\)中此時最高的\(1\)所在的位置(設其為第\(j\)位,若沒有則\(j=-1\))。

顯然\(B\ge 2^j\),那麼\(2^{j-1},2^{j-2},...,2^0\)肯定也都在\([0,B]\)的範圍中。

也就是說,僅在\([0,B]\)中選擇,我們可以得到的值除去第\(i\)位之後的範圍是\([0,2^{j+1})\)

那麼現在就變成了在\([0,2^{j+1})\)\([A,2^i)\)兩個區間中任選數。

一種情況是\(A\le2^{j+1}\),那麼\([0,2^i)\)中的所有數都能得到,因此\(2^i\)就是這種情況下的答案

另一種情況是\(A>2^{j+1}\),對於\([0,2^{j+1})\)中的數已經達到了它們的極限,而\([A,2^i)\)其實和前面第\(i\)位為\(0\)的情況一樣,因此\(2^{j+1}+(2^i-A)\)就是這種情況下的答案

程式碼實現真的非常簡潔。

程式碼:\(O(logB)\)

#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 LL long long
using namespace std;
int main()
{
	LL A,B;if(scanf("%lld%lld",&A,&B),A==B) return puts("1"),0;//如果A=B
	RI i;for(i=60;!((A^B)>>i&1);--i) A>>i&1&&(A^=1LL<<i,B^=1LL<<i);LL t=(1LL<<i)-A;//消去相同字首,t記錄第i位為0的答案
	RI j=i-1;W(~j&&!(B>>j&1)) --j;if(A<=(1LL<<j+1)) return printf("%lld\n",t+(1LL<<i)),0;//找到此時最高位j,若能全取到
	return printf("%lld\n",(t<<1)+(1LL<<j+1)),0;//若不能全取到
}

E:Mr.Aoki Incubator(點此看題面

  • 數軸上有\(n\)個點,每個點有一個初始位置\(p_i\)以及一個固定的向正方向移動的速度\(v_i\)
  • 初始時你可以選中若干點染色,一個染色點會給所有碰到的點都染上色。
  • 求有多少種方案,會使得最終所有點都被染上了色。
  • \(n\le2\times10^5\)

按初始位置排序

我們先把所有點按初始位置排個序,再作思考。

一個非常基礎也非常重要的問題,就是一個點不光可能被直接染色,也可能被間接染色(就是一個未被選中的點經過了某個被選中的點,再經過了當前點)。

先只考慮一個點\(x\)被它後面的點\(y\)染色的情況,就按上面的說法分成兩種:

  • 直接染色:要求滿足\(v_x<v_y\)
  • 間接染色:設原先被選中染色的點為\(z\)。則\(v_z<v_x<v_y,p_z>p_y\)

然後發現,不管如何,都要滿足\(v_x<v_y\)

再仔細一想,發現只要存在一個滿足\(v_x<v_y\)\(y\),使得\([p_y,p_x]\)之中存在一個點被染過色,那麼\(x\)就會被染色。

最後結合\(x\)被它前面的點染色的情況,總結一下,就是要找到最小的滿足\(v_x<v_l\)的點,以及最大的滿足\(v_x>v_r\)的點,只要\([l,r]\)中有一個點被染過色,那麼\(x\)就會被染色。

至於如何求出這個區間,直接維護一個棧,然後在上面二分就可以了。

動態規劃

現在就變成了這樣一個問題:存在若干個區間,要求每個區間中至少被選中一個點,求方案數。

\(f_i\)表示最後一個點選中\(i\)的方案數。

假設要轉移到\(f_j\),顯然,因為每個區間中都至少被選中一個點,所以\([i+1,j-1]\)之間不能存在一個完整的限制區間

因此我們找到所有左端點大於\(i\)的區間,那麼\(j\)不能大於它們右端點的最小值(設其為\(Mx_i\))。

也就是說\(f_i\)可以轉移到\([i+1,Mx_i]\),也就是給這段區間的\(f_j\)都加上\(f_i\)

直接差分一下就好了。

程式碼:\(O(nlogn)\)

#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 200000
#define X 1000000007
#define Inc(x,y) ((x+=(y))>=X&&(x-=X))
using namespace std;
int n,S[N+5],f[N+5],g[N+5],Mx[N+5];
struct line {int k,b;I bool operator < (Con line& o) Con {return b<o.b;}}s[N+5];
struct P {int l,r;I bool operator < (Con P& o) Con {return l<o.l;}}p[N+5];
I int F(RI l,RI r,CI x) {RI mid;W(l^r) s[S[mid=l+r>>1]].k>=x?r=mid:l=mid+1;return r;}//在棧中[l,r]上二分,求出第一個大於等於x的位置
int main()
{
	RI i,x,y;for(scanf("%d",&n),i=1;i<=n;++i) scanf("%d%d",&s[i].b,&s[i].k);sort(s+1,s+n+1);
	RI T=0;for(i=1;i<=n;++i)//從前往後
		p[i].l=s[S[T]].k<=s[i].k?i:S[F(1,T,s[i].k+1)],s[S[T]].k<s[i].k&&(S[++T]=i);
	for(s[0].k=2e9,T=n+1,i=n;i;--i)//從後往前
		p[i].r=s[S[T]].k>=s[i].k?i:S[F(T,n+1,s[i].k)-1],s[S[T]].k>s[i].k&&(S[--T]=i);
	RI j,t=n+1;for(sort(p+1,p+n+1),i=j=n;~i;Mx[i--]=t) W(p[j].l>i) t=min(t,p[j--].r);//倒枚一遍預處理Mx[i]
	for(f[0]=1,i=0;i<=n+1;++i) i&&Inc(g[i],g[i-1]),
		Inc(f[i],g[i]),Inc(g[i+1],f[i]),Inc(g[Mx[i]+1],X-f[i]);//差分打標記轉移
	return printf("%d\n",f[n+1]),0;//輸出答案
}

F:Kenus the Ancient Greek(點此看題面

  • 定義\(F(i,j)\)表示求\(gcd(i,j)\)時的輾轉相除次數。
  • 對於所有\(i\in[1,A],j\in[1,B]\),求\(F(i,j)\)的最大值以及能取到最大值的\(i,j\)組數。
  • 資料組數\(\le3\times10^5,A,B\le10^{18}\)

斐波那契數

\(A=B\)時,顯然我們可以通過斐波那契數來卡到\(F(i,j)\)的上界。

實際上,當\(A\le B\)時,我們依然可以利用斐波那契數,只要找到最大的\(t\)滿足\(Fib(t)\le A,Fib(t+1)\le B\),那麼答案就是\(t\)

因此第一問其實是很好做的,於是主要就是要考慮第二問,即方案數。

大膽猜結論

首先我們先強制它執行一次輾轉相除,不妨設得到\(a,b\)

考慮斐波那契遞推式\(Fib(i)=Fib(i-1)+Fib(i-2)\),那麼我們所能做的就是對於若干\(Fib(i)\)修改一下遞推式的係數得到\(Fib'(i)=kFib'(i-1)+Fib'(i-2)\)

接著我們發現,\(a\)必須在\([Fib(t),A]\)範圍內,因此它的上界是卡得非常緊的。

具體地,我們發現,對於一個表示式,如果我們令\(k>2\)了,由於\(2Fib(i-1)>Fib(i)\),那麼最後的值至少翻倍,必然超過\(Fib(t+1)\),更超過\(A\)

然後我們考慮\(\frac{Fib(i+1)}{Fib(i)}≈1.618\),如果修改的表示式個數\(>2\),由於\((\frac{Fib(i+1)}{Fib(i)})^2>2\),那麼最後的值也至少翻倍,必然超過\(Fib(t+1)\),更超過\(A\)

所以,得出結論,最多隻會修改一個表示式,且只會把某一個\(k\)改成\(2\)

剩下的就很簡單了。

程式碼:\(O(88T)\)

#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 LL long long
#define X 1000000007
using namespace std;
int t;LL A,B,Fib[88],f[88][2];
I int Solve(LL A,LL B)//求答案
{
	#define Calc(a,b) (B>=b?((B-b)/a+1)%X:0)//計算在[b,B]中與b模a同餘的數的個數
	RI i,ans=Calc(Fib[t],Fib[t+1]);LL a,b;for(i=1;i^t;++i)
		a=f[t-i][0]*Fib[i]+f[t-i][1]*(Fib[i-1]+2*Fib[i]),//計算a
		b=f[t-i+1][0]*Fib[i]+f[t-i+1][1]*(Fib[i-1]+2*Fib[i]),//計算b
		a<=A&&b<=B&&(ans=(ans+Calc(a,b))%X);
	return ans;
}
int main()
{
	RI Tt,i;for(Fib[0]=Fib[1]=f[0][0]=f[1][1]=1,i=2;i^88;++i)
		Fib[i]=Fib[i-2]+Fib[i-1],f[i][0]=f[i-2][0]+f[i-1][0],f[i][1]=f[i-2][1]+f[i-1][1];//預處理斐波那契數以及每一項中Fib(0)和Fib(1)的貢獻
	scanf("%d",&Tt);W(Tt--)
	{
		scanf("%lld%lld",&A,&B),A>B&&(swap(A,B),0);//強制A≤B
		if(A==1||B<=2) {printf("1 %d\n",A*B%X);continue;}//特判小情況
		t=upper_bound(Fib+1,Fib+88,A)-Fib-1,Fib[t+1]>B&&--t,//求出t
		printf("%d %d\n",t,(Solve(A,B)+Solve(A,A))%X);
	}return 0;
}