1. 程式人生 > 其它 >Codeforces Round #752 (Div. 1)

Codeforces Round #752 (Div. 1)

前言

比賽之前我就想著先開 \(D\),然後肝了 \(1.8\) 個小時終於搞出來了,因為我是慫包所以不敢用大號交,用小號搶了 \(\tt Div2F\) 的首 \(A\)(好像賽時很少人做出來),就不想打了。

下次還是要相信自己的實力,自信即顛峰,\(3000\) 的題我不只切了一次兩次了。不要畏懼難題,一發上紅不是問題。

C. Extreme Extension

題目描述

定義操作為把數 \(a\) 拆成兩個和等於 \(a\) 的正整數,定義序列的價值為最小的操作次數使之不降。

\(n\) 個數 \(a_i\),問每一個子區間的價值和,答案對 \(998244353\) 取模。

\(n\leq 10^5,a_i\leq 10^5\)

解法

首先考慮對於一個給定的區間如何計算價值。可以從後往前考慮,每一次都儘量大的拆分,這樣的運算元是最小的,並且於後面也是最優的。設現在的開頭是 \(x\),那麼個數 \(k=\lceil\frac{a_i}{x}\rceil\),新的開頭 \(x'=\lfloor\frac{a_i}{k}\rfloor\)

對於每個 \(i\)\(k\) 的取值只有 \(\sqrt n\) 種,那麼 \(x'\) 的取值也只有 \(\sqrt n\) 種。因為計算只和開頭的數字有關,可以把它當成狀態記錄下來,我們使用整體 \(dp\) 的技巧,也就是在所有右端點處初始化,在左端點處統計答案,只需要做一次 \(dp\)

就可以解決問題。

\(dp[i][j]\) 表示考慮到 \(i\) 開頭的數字是 \(j\) 的方案數,時間複雜度 \(O(n\sqrt n)\)

總結

區間統計問題也可以考慮整體 \(dp\) 的技巧,考慮需要記錄什麼東西把統計問題轉 \(dp\)

#include <cstdio>
#include <vector>
#include <iostream>
using namespace std;
const int M = 100005;
const int MOD = 998244353;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int T,n,w,ans,a[M],f[2][M];vector<int> v[2];
signed main()
{
	T=read();
	while(T--)
	{
		n=read();w=ans=0;
		for(int i=1;i<=n;i++)
			a[i]=read();
		for(int i=n;i>=1;i--)
		{
			w^=1;int ls=a[i];
			v[w].push_back(a[i]);
			f[w][a[i]]=1;
			for(auto x:v[w^1])
			{
				int k=(a[i]+x-1)/x,to=a[i]/k;
				f[w][to]=(f[w][to]+f[w^1][x])%MOD;
				ans=(ans+(k-1)*i*f[w^1][x])%MOD;
				if(to^ls) v[w].push_back(to),ls=to;
			}
			for(auto x:v[w^1]) f[w^1][x]=0;
			v[w^1].clear();
		}
		for(auto x:v[w]) f[w][x]=0;v[w].clear();
		printf("%lld\n",ans);
	}
}

D. Artistic Partition

題目描述

定義 \(c(l,r)\) 表示滿足 \(l\leq i\leq j\leq r\) 並且 \(\gcd(i,j)\geq l\)\((i,j)\) 對數。

定義 \(f(n,k)\) 為一個劃分 \(0=x_1<x_2<...x_{k+1}=n\) 最小的 \(\sum_{i=1}^k c(x_i+1,x_{i+1})\)

多組資料,求 \(f(n,k)\)

\(T\leq 3\cdot 10^5,1\leq k\leq n\leq 10^5\)

解法

首先我想了一個 \(O(n^4k)\)\(dp\),打了個表發現沒什麼規律,然後開始瘋狂優化。

我發現 \(k\) 較小的時候答案就趨近 \(n\) 了,設左端點是 \(l\),那麼 \([l,2l)\) 這一段的額外代價為 \(0\),所以說最多隻需要 \(\tt log\) 次劃分就可以使得答案達到最小值。

再考慮怎麼優化列舉前驅的過程,可以盲猜一波有決策單調性,也就是滿足下面的式子:

\[c(i,j+1)+c(i+1,j)\geq c(i+1,j+1)+c(i,j) \]

經我打表驗證加上感性理解,可以證明上面的式子的成立的,那麼套一個決策單調性分治即可。剩下的問題就是快速求 \(c(l,r)\) 了,比賽時我就是因為這個卡了很久,其實直接莫比烏斯反演即可:

\[\begin{aligned}c(l,r)&=\sum_{i=l}^r\sum_{j=i+1}^r[\gcd(i,j)\geq l]\\&=\sum_{k=l}^{r}\sum_{i=l}^r\sum_{j=i+1}^r[\gcd(i,j)=k]\\&=\sum_{k=l}^{r}\sum_{i=1}^{r/k}\sum_{j=i+1}^{r/k}[\gcd(i,j)=1]\\&=\sum_{k=l}^r\sum_{i=1}^{r/l}\phi(i)\\&=\sum_{k=l}^r sum(\lfloor\frac{r}{i}\rfloor)\end{aligned} \]

所以求出尤拉函式的字首和之後整除分塊即可,在一段轉移區間內求值的時候只需要把左端點拿出去做整除分塊,剩下的可以略微修改推出來,時間複雜度我也不太清楚,因為是預處理所以該演算法穩定在 \(1s\) 左右。

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int M = 100005;
#define int long long 
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int T,n,k,cnt,p[M],ph[M],dp[22][M];
void init(int n)
{
	ph[1]=1;
	for(int i=2;i<=n;i++)
	{
		if(!ph[i]) ph[i]=i-1,p[++cnt]=i;
		for(int j=1;j<=cnt && i*p[j]<=n;j++)
		{
			if(i%p[j]==0)
			{
				ph[i*p[j]]=ph[i]*p[j];
				break;
			}
			ph[i*p[j]]=ph[i]*(p[j]-1);
		}
	}
	for(int i=1;i<=n;i++) ph[i]+=ph[i-1];
}
int cal(int m,int n)
{
	int res=0;
	for(int l=m,r=0;l<=n;l=r+1)
	{
		r=n/(n/l);
		res+=ph[n/l]*(r-l+1);
	}
	return res;
}
void cdq(int l,int r,int L,int R)
{
	if(l>r) return ;
	int mid=(l+r)>>1,c=cal(L+1,mid),p=0;
	for(int i=L;i<=min(R,mid);i++)
	{
		if(dp[k][mid]>dp[k-1][i]+c)
		{
			dp[k][mid]=dp[k-1][i]+c;
			p=i;
		}
		c-=ph[mid/(i+1)];
	}
	cdq(l,mid-1,L,p);
	cdq(mid+1,r,p,R);
}
signed main()
{
	n=100000;init(n);
	memset(dp,0x3f,sizeof dp);dp[0][0]=0;
	for(k=1;k<=20;k++) cdq(1,n,0,n-1);
	T=read();
	while(T--)
	{
		n=read();k=read();
		if(k>20) printf("%lld\n",n);
		else printf("%lld\n",dp[k][n]);
	}
}

E. A Perfect Problem

題目描述

定義一個序列是好的,當且僅當最大值乘上最小值大於等於權值和。

定義一個長度為 \(n\) 的序列 \(a\) 是完美的,當且僅當 \(1\leq a_i\leq n+1\),並且它的所有子序列都是好的。

給出 \(n\) 和質數 \(m\),求長度為 \(n\) 的完美序列 \(a\) 有模 \(m\) 下有多少個。

\(n\leq 200,10^8\leq m\leq 10^9\)

解法

\(\cdot\) 結論題,本題最關鍵的條件就是 \(1\leq a_i\leq n+1\)(這限制夢迴 \(\tt NOI2020\)

\(\tt Observation\ one\):可以貪心轉化判據,我們把 \(a\) 序列從小到大排序,如果最大值是 \(a_i\) 那麼最小值一定是 \(a_1\),那麼所有子序列合法就轉化成了所有前驅合法。

\(\tt Observation\ two\):我們可以只考慮排序之後的序列,然後用簡單組合數就可以計算出原序列,排序後序列需要滿足 \(a_i\geq i\),否則 \(a_i\cdot a_1<i\cdot a_i\leq \sum_{j=1}^i a_j\) 顯然不合法。

\(\tt Observation\ three\):如果 \(a_i=i\),那麼 \(\forall j\leq i,a_j=i\),因為 \(a_i\cdot a_1=i\cdot a_1\leq \sum_{j=1}^i a_j\),所以當且僅當所有數等於 \(i\) 時取等,此時才能滿足條件。

基於這個觀察和對題設的分析,我們可以知道當 \(a_n=n\) 的時候唯一對應一種合法序列 ,所以可以假設 \(a_n=n+1\) 以方便下面的討論。

\(\tt Observation\ four\):假設 \(a_n=n+1\),如果 \(a_i\geq i+1\),那麼這個字首自動合法。因為我們知道 \(a_1\cdot a_n\geq\sum_{i=1}^na_i\),所以 \(a_1\geq \sum_{i=1}^n a_i-a_1\),推出 \(a_1\geq \sum_{j=1}^i a_j-a_1\),所以前綴合法。


綜合上文所有的觀察,我們可以知道假設 \(a_n=n+1\),序列合法的充要條件是:

  • \(\forall i\leq a_1,a_1\leq a_i\leq n+1\)
  • \(\forall i>a_1,i+1\leq a_i\leq n+1\)
  • \(\sum_{i=1}^n a_i-a_1\leq a_1\)

我們列舉 \(a_1\) 之後,可以去規劃差值序列 \(b_i=a_i-a_1\),序列 \(b\) 合法的充要條件是:

  • \(0\leq b_i\leq n+1-a_1\)
  • \(\sum_{i=1}^n b_i\leq a_1\)
  • 至少有 \(1\) 個數大於等於 \(n+1-a_1\),至少有 \(2\) 個數大於等於 \(n-a_1.....\)至少有 \(n-a_1\) 個數大於等於 \(2\)

\(dp[i][j][k]\) 表示考慮到權值 \(k\),已經在序列裡放置了 \(i\) 個數,現在的總和是 \(j\),轉移列舉新加數的權值有多少個,不難發現這是個調和級數,所以時間複雜度 \(O(n^4\log n)\)

\(\tt Observation\ seven\):有用的 \(a_1\) 只有後 \(2\sqrt n\) 種,所以時間複雜度 \(O(n^3\sqrt n\log n)\)

#include <cstdio>
#include <cstring>
#include <iostream>
#include <cassert>
using namespace std;
const int M = 205;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,MOD,a1,ans,fac[M],inv[M],f[M][M][M],v[M][M][M];
int qkpow(int a,int b)
{
	int r=1;
	while(b>0)
	{
		if(b&1) r=r*a%MOD;
		a=a*a%MOD;
		b>>=1;
	}
	return r;
}
int dfs(int i,int s,int k)
{
	if(i==n) return fac[n];
	if(k==0) return fac[n]*inv[n-i]%MOD;
	if(v[i][s][k]==a1) return f[i][s][k];
	int &r=f[i][s][k];r=0;v[i][s][k]=a1;
	for(int j=(a1-s)/k;j>=0;j--)
	{
		if(k>1 && i+j<n-a1+1-k+1) continue;
		r=(r+dfs(i+j,s+j*k,k-1)*inv[j])%MOD;
	}
	return r;
}
signed main()
{
	n=read();MOD=read();fac[0]=1;
	for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%MOD;
	for(int i=0;i<=n;i++) inv[i]=qkpow(fac[i],MOD-2);
	for(a1=max(1ll,n-30);a1<=n;a1++)
		ans=(ans+dfs(0,0,n+1-a1))%MOD;
	printf("%lld\n",ans);
}