我的動態規劃題單

Green&White發表於2024-09-02

可惡的動態規劃,每次考試基本都寫不出來,於是特意整理個動態規劃提單
因為部落格園好像標題和網址不能同時用,所以本來點標題就可以跳轉了,現在要自己去搜,大多是洛谷的題,搜不到就是內部題。


1.CF1620F Bipartite Array

題意等價於:要把這些點分成兩部分,每一部分之間都沒有邊相連,等價於把這個序列中分成兩個上升子序列。

在DP時肯定要記錄兩個序列的末尾,但發現其中一個序列的末尾肯定是 \(a[i]\) 或者 \(-a[i]\) , 因此只需記錄另外一個的

\(f[i][0/1][j]\) 表示把 \(a[i]\) 不取反/取反 作為一個序列的末尾,另一個序列的末尾是 \(j\) 是否可行
由於這個狀態值僅僅是可不可行,第三維又很大,於是可以考慮把第三維記錄狀態:
\(f[i][0/1]\) 表示 把 \(a[i]\) 不取反/取反 作為一個序列的末尾,另一個序列的末尾的最小值(顯然另一個序列末尾越小越優)。

在轉移時看下一個數放在哪個序列末尾,並記錄一下方案即可。

寫的可能有點麻煩

code

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5,inf=0x3f3f3f3f;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int T,n,a[N],f[N][2],ans[N][2];
bool check(int i,int op){
	if(f[i][op]==inf) return false;
	if(i==1) return true; 
	return check(i-1,ans[i][op]);
}
void print(int i,int op){
	if(i!=1) print(i-1,ans[i][op]);
	if(op==0) printf("%d ",a[i]);
	else printf("%d ",-a[i]);
}
signed main(){
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	T=read();
	while(T--){
		n=read();
		for(int i=1;i<=n;i++) a[i]=read();
		for(int i=1;i<=n;i++) f[i][0]=f[i][1]=inf;
		f[1][0]=f[1][1]=-inf;
		for(int i=2;i<=n;i++){
			//轉移f[i][0] 
			if(a[i]>a[i-1]&&f[i-1][0]!=inf){
				if(f[i-1][0]<f[i][0]){
					f[i][0]=min(f[i][0],f[i-1][0]);
					ans[i][0]=0;
				}
			} 
			if(a[i]>f[i-1][0]&&f[i-1][0]!=inf){
				if(a[i-1]<f[i][0]){
					f[i][0]=min(f[i][0],a[i-1]);
					ans[i][0]=0;
				}
			} 
			if(a[i]>-a[i-1]&&f[i-1][1]!=inf){
				if(f[i-1][1]<f[i][0]){
					f[i][0]=min(f[i][0],f[i-1][1]);
					ans[i][0]=1;
				}
			} 
			if(a[i]>f[i-1][1]&&f[i-1][1]!=inf){
				if(-a[i-1]<f[i][0]){
					f[i][0]=min(f[i][0],-a[i-1]);
					ans[i][0]=1;
				}
			} 
			//轉移f[i][1] 
			if(-a[i]>a[i-1]&&f[i-1][0]!=inf){
				if(f[i-1][0]<f[i][1]){
					f[i][1]=min(f[i][1],f[i-1][0]);
					ans[i][1]=0;
				}
			} 
			if(-a[i]>f[i-1][0]&&f[i-1][0]!=inf){
				if(a[i-1]<f[i][1]){
					f[i][1]=min(f[i][1],a[i-1]);
					ans[i][1]=0;
				}
			} 
			if(-a[i]>-a[i-1]&&f[i-1][1]!=inf){
				if(f[i-1][1]<f[i][1]){
					f[i][1]=min(f[i][1],f[i-1][1]);
					ans[i][1]=1;
				}
			} 
			if(-a[i]>f[i-1][1]&&f[i-1][1]!=inf){
				if(-a[i-1]<f[i][1]){
					f[i][1]=min(f[i][1],-a[i-1]);
					ans[i][1]=1;
				}
			} 
		}
		if(f[n][0]!=inf||f[n][1]!=inf){
			printf("YES\n");
			if(check(n,1)) print(n,1);
			else print(n,0);
			puts("");
		}
		else puts("NO");
	}
	return 0;
}


2.CF1616H Keep XOR Low

看到位運算,直接就放到 Trie 裡面。

一個很自然的想法:設 \(f[i]\) 表示第 \(i\) 棵子數內的方案數
如果當前考慮到了第 \(j\) 位:

  1. 如果 \(x\) 的第 \(j\) 位為 \(0\),那顯然左右子樹不能都選,直接 \(f[i]=f[ls]+f[rs]\)
  2. 如果 \(x\) 的第 \(j\) 位為 \(1\),這時候就出問題了,因為雖然每棵子樹內部可以隨便選,但是同時選兩棵子樹的情況很難轉移。

於是大膽一點,設 \(f[u][v]\) 表示在 \(u\) 子樹和 \(v\) 子樹分別選幾個數(可以為空)使他們兩兩之間滿足條件的方案數( \(u\) 可以等於 \(v\))
注意:對 \(u\)\(v\) 自身子樹裡選數沒有限制,即只需滿足跨子樹的限制,可以這樣設計是因為當 \(u \ne v\) 的時候意味著 \(u\)\(v\) 這棵子樹內的點異或起來的結果在更高的位上已經比 \(x\) 小了

如果當前考慮到了第 \(j\) 位:

  1. 如果 \(x\) 的第 \(j\) 位為 \(1\):
    同時選 \(u\) 的左兒子和 \(v\) 的左兒子顯然是一定滿足的,同時選右兒子同理
    於是只需要把選 \(u\) 的左兒子和 \(v\) 的右兒子 和 選 \(u\) 的右兒子和 \(v\) 的左兒子 的方案數乘起來
    是乘法原理,因為這兩步之間是沒有限制的

  2. 如果 \(x\) 的第 \(j\) 位為 \(0\):
    此時只能同時選 \(u\) 的左兒子和 \(v\) 的左兒子或同時選右兒子
    那就是加法原理,因為這兩步如果同時發生會出現 既選了 \(u\) 的左兒子又選了 \(v\) 的右兒子的情況
    但注意這裡還有一種額外的情況:
    因為我們 \(f\) 陣列的定義只考慮跨界的異或情況,所以我們還可以只選 \(u\) 的點或只選 \(v\) 的點 ,要額外加上

每個點只會被遍歷一遍,故時間複雜度是對的

code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=150000+5,M=6e6+5,mod=998244353;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,x;
int tot=1,ch[M][3],Size[M],mi[M];
void insert(int x){
	int p=1;
	for(int i=30;i>=0;i--){
		int c=(x>>i)&1;
		Size[p]++;
		if(!ch[p][c]) ch[p][c]=++tot;
		p=ch[p][c];
	}
	Size[p]++;
}
int dfs(int u,int v,int d){   //這個計算出的結果可以有空集 
	if(!u) return mi[Size[v]];
	if(!v) return mi[Size[u]];
	if(u==v){
		if(d==-1) return mi[Size[u]];    //到達葉子 
		int ls=ch[u][0],rs=ch[u][1];
		if(x>>d&1) return dfs(ls,rs,d-1);
		else return (dfs(ls,ls,d-1)+dfs(rs,rs,d-1)-1ll+mod)%mod;  //空集會被算兩次所以-1 
	}
	if(d==-1) return mi[Size[u]+Size[v]];   
	int ls1=ch[u][0],ls2=ch[v][0],rs1=ch[u][1],rs2=ch[v][1];
	if(x>>d&1) return dfs(ls1,rs2,d-1)*dfs(rs1,ls2,d-1)%mod;
	else{
		int ans=(dfs(ls1,ls2,d-1)+dfs(rs1,rs2,d-1)-1ll+mod)%mod;
		(ans += ( mi[Size[ls1]] - 1ll + mod ) * ( mi[Size[rs1]] -1ll + mod ) )%=mod;
		(ans += ( mi[Size[ls2]] - 1ll + mod ) * ( mi[Size[rs2]] -1ll + mod ) )%=mod;
		  //要減掉左右子樹中有一個不選的情況,因為(dfs(ls1,ls2,d-1)+dfs(rs1,rs2,d-1)-1ll+mod)算過了 
		return ans; 
	} 
}
signed main(){
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	n=read(),x=read();
	for(int i=1;i<=n;i++) insert(read());
	mi[0]=1;
	for(int i=1;i<=n;i++) (mi[i]=mi[i-1]*2ll)%=mod;
	printf("%lld\n",(dfs(1,1,30)-1ll+mod)%mod);  //不能是空集 
	return 0;
}


3.CF1775F Laboratory on Pluto

最小周長的圖形一定是個凸的圖形而不是凹的,否則一定不優。

對於一個凸的圖形,他的周長可以透過平移轉化成能夠包含他的最小矩形的周長,比如:

  1. 第一問:
    假設最小矩形邊長為 \(a\) , \(b\) ,則需滿足 \(a \times b \ge n\) (因為要往裡面填 \(n\) 個方塊),在此基礎上使 \(a+b\) 儘可能小。
    假設我們已經確定了 \(a\times b\)的值,那顯然 \(a\) , \(b\) 越接近,\(a+b\)越小(小學數學)
    具體證明就是\((a+b)^2=(a-b)^2+4ab\)....
    那肯定 \(a\) , \(b\) 都取 \(\sqrt{n}\) 最優,當然不一定是整數,自己隨便湊一湊就可以了.
    其實假設 \(a<b\) , 則 $ a \le \sqrt{n} $, \(O(n \sqrt{n} )\)列舉可過,構造方案的話往裡面隨便填 \(n\)# 即可
  2. 第二問:
    一個凸的圖形一定是由那個最小矩形挖去四個角得到的,且挖去的角一定是梯形
  • 先 DP 求出面積為 \(i\) 的梯形的方案數,具體來講:
    \(g[i][j]\)表示面積為\(i\),一共有\(j\)列的梯形的方案數,我們規定梯形每一列的方塊數單調不增
    那要麼新增一列,要麼每一列都加一個方塊:\(g[i][j]=g[i-1][j-1]+g[i-j][j]\)
  • \(sum[i]\)表示面積為\(i\)的梯形的方案數,則 \(sum[i]= \sum_{j = 1}^{i} g[i][j]\)
    因為挖去的方塊總數一定不會大於一條邊長(否則完全可以縮小矩形),所以\(i,j<=\sqrt{n}\)
  • 然後 DP 四個角的情況:
    \(f[i][j]\)表示挖去\(j\)個角,一共挖去\(i\)個方塊的方案數,則\(f[i][j]=f[i-k][j-1] \times sum[k]\)
    還是因為挖去的方塊總數一定不會大於一條邊長,所以四個角一定不會相交。

但是比如當\(n=8\)時,\(2 \times 4\)\(3 \times 3\)的邊長一樣是最小的,所以我們這裡需要\(O(C)\)列舉邊長,\(C\)表示周長。
注意這裡千萬不要\(O(n)\)列舉邊長,因為當\(u=2\)時沒有保證\(n \text{的和} \le 8 \times 10^5\)

code

#include<bits/stdc++.h>
using namespace std;
const int N=1e3+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int T,n,u,mod,g[N][N],sum[N],f[N][N]; 
signed main(){
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	T=read(),u=read();
	if(u==1){
		while(T--){
			n=read();
			int a=sqrt(n),b=(n%a==0)?(n/a):(n/a+1);
			printf("%d %d\n",a,b);
			for(int i=1;i<=a;i++){
				for(int j=1;j<=b;j++){
					if((i-1)*b+j<=n) putchar('#');
					else putchar('.');
				}
				puts("");
			}
		}
	}
	else{
		mod=read();
		g[0][0]=1;
		for(int i=1;i<N;i++){
			for(int j=1;j<=i;j++){
				(g[i][j]=g[i-1][j-1]+g[i-j][j])%=mod;
			}
		}
		for(int i=0;i<N;i++){
			for(int j=0;j<=i;j++){
				(sum[i]+=g[i][j])%=mod;
			}
			f[i][1]=sum[i];
		}
		for(int j=2;j<=4;j++){
			for(int i=0;i<N;i++){
				for(int k=0;k<=i;k++){
					(f[i][j]+=1ll*f[i-k][j-1]*sum[k]%mod)%=mod;
				}
			}
		}
		while(T--){
			n=read();
			int a=sqrt(n),b=(n%a==0)?(n/a):(n/a+1),c=a+b,ans=0;
			for(int i=1;i<=c;i++){
				int j=c-i;
				if(i*j>=n) (ans+=f[i*j-n][4])%=mod; 
			}
			printf("%d %d\n",c*2,ans);
		}
	}
	return 0;
}

4.AT_agc002_f [AGC002F] Leftmost Ball

因為每一種球有個數限定,為了避免麻煩,我們在每一次放一種顏色的球時選擇把所有對應顏色的球都放進去,具體來講:
\(f[i][j]\) 表示目前放了 \(i\) 個白球和 \(j\) 種顏色的球的方案數
一種合法的方案一定滿足對於任意一個字首白球數量大於等於放的顏色種類,即 \(i \ge j\)
對於當前放球的狀態,我們找到第一個沒有被放球的位置(空位和非空位可能交替出現):

  1. 如果放白球那就轉移到 \(f[i+1][j]\)
    注意放白球的時候我們並不把顏色種類\(j+1\)
  2. 如果選擇放不是白色的球 (此時需滿足\(i>j\))
    我們已經放了 \(j\) 種,還剩 \(n-j\) 種可以放的顏色球,除去那個白色的球和一定要放在這個位置上的球,每種球剩餘\(k-2\) ,要在在剩餘 $ n \times k - i - j \times (k-1) - 1$ 個位置裡選出 \(k-2\)
    則用 \(f[i][j] \times (n-j) \times C_{n \times k - i - j \times (k-1) - 1}^{k-2}\) 轉移到 \(f[i][j+1]\)

code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e3+5,mod=1e9+7,M=4e6+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,k,f[N][N];
int fac[M],inv[M],q[M];
int C(int n,int m){
	return fac[n]*q[m]%mod*q[n-m]%mod;
}
signed main(){
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	n=read(),k=read();
	fac[0]=1;
	for(int i=1;i<M;i++) fac[i]=fac[i-1]*i%mod;
	inv[1]=1;
	for(int i=2;i<M;i++)
		inv[i]=(mod-mod/i)*inv[mod%i]%mod;
	q[0]=1;
	for(int i=1;i<M;i++)
		q[i]=q[i-1]*inv[i]%mod;
	f[1][0]=1;
	for(int i=1;i<=n;i++){
		for(int j=0;j<=i;j++){
			(f[i+1][j]+=f[i][j])%=mod;
			if(i>j) (f[i][j+1]+=f[i][j]*(n-j)%mod*C(n*k-i-j*(k-1)-1,k-2)%mod)%=mod;
		}
	}
	if(k==1) printf("%lld\n",f[n][0]); //特判k=1 
	else printf("%lld\n",f[n][n]);
	return 0;
} 

5.CF1615F LEGOndary Grandmaster

先考慮怎麼計算兩個01串的距離
一個變換:把原串所有偶數位上的數取反,這樣$ \text{取反兩個原串中相同且相鄰的數} = \text{交換兩個新串中相鄰的數} $
比如原串 從001101變成000001
那麼新串就從 011000 -> 010100
並且如果原串中,兩個相鄰的數不同,那它們在新串中就是相同的,交換他們和沒交換一樣

所以問題轉化成:給定兩個01串\(S\),\(T\) ,求最少的交換次數使得第一個串變成第二個
顯然有解的充要條件是\(S,T\)\(1\)的個數相同
\(a_i\)表示\(S[1,i]\)\(1\)的個數,\(b_i\)表示\(T[1,i]\)\(1\)的個數,答案就是 \(\sum_{i=1}^{n} |ai-bi|\)
證明:
1.首先證明這是個下界,因為最終狀態是 $ \sum_{i=1}^{n} |ai-bi| =0 $ 每一次交換 \(i\)\(i+1\) ,只會影響 \(a\)\(a_i\) 的值,且 \(|ai-bi|\) 的值至多 \(-1\) ,所以答案至少是 \(\sum_{i=1}^{n} |ai-bi|\)
2.可行性:\(|ai-bi|\)其實計算的就是最終跨過\(i\)這條分界線進行交換的\(1\)的個數

\({\color{red} {這個結論很重要}}\)

然後就是又一個套路:每個位置分別計算貢獻
現在\(S,T\)是題目中的\(S,T\)了,即有?
\(f[i][j]\)表示使得\(a_i-b_i=j\)的方案數,\(g[i][j]\)表示使得$s[i,n]中 \(1\) 的個數-t[i,n]中1的個數=j\(的方案數 這個轉移\)O(n^2)\(很顯然 所以\)ans=\sum_{i=1}^{n} \sum_{j=-n}^{n}f[i][j] \times g[i+1][-j] \times |j|$
因為要滿足 \(S,T\)\(1\) 個數相同
因為下標不可以是負數,所以要偏移量

code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e3+5,mod=1e9+7;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int T,n;
string s,t;
int a[N],b[N];
int f[N][N<<1],g[N][N<<1];
signed main(){
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	T=read();
	while(T--){
		n=read();
		cin>>s>>t;
		for(int i=1;i<n;i+=2){
			if(s[i]=='0') s[i]='1';
			else if(s[i]=='1') s[i]='0';
			if(t[i]=='0') t[i]='1';
			else if(t[i]=='1') t[i]='0';
		}
		s=' '+s,t=' '+t;
		for(int i=1;i<=n;i++){
			if(s[i]!='?') a[i]=s[i]-'0';
			if(t[i]!='?') b[i]=t[i]-'0';	
		} 
		for(int i=0;i<=n+1;i++){
			for(int j=0;j<=2*n+2;j++){
				f[i][j]=g[i][j]=0;
			}
		}
		f[0][n]=1;
		for(int i=1;i<=n;i++){
			for(int j=-i;j<=i;j++){
				if(s[i]!='?'&&t[i]!='?') f[i][j+n] = f[i-1][j - (a[i]-b[i]) + n];
				else if(s[i]!='?'&&t[i]=='?') f[i][j+n] = ( f[i-1][j - (a[i]-0) + n] + f[i-1][j - (a[i]-1) + n])%mod;
				else if(s[i]=='?'&&t[i]!='?') f[i][j+n] = ( f[i-1][j - (0-b[i]) + n] + f[i-1][j - (1-b[i]) + n])%mod;
				else f[i][j+n] = ( ( f[i-1][j-1+n] + 2*f[i-1][j+n] ) % mod + f[i-1][j+1+n] )%mod;
			}
		}
		g[n+1][n]=1;
		for(int i=n;i>=1;i--){
			for(int j=-(n-i+1);j<=n-i+1;j++){
				if(s[i]!='?'&&t[i]!='?') g[i][j+n] = g[i+1][j - (a[i]-b[i]) + n];
				else if(s[i]!='?'&&t[i]=='?') g[i][j+n] = ( g[i+1][j - (a[i]-0) + n] + g[i+1][j - (a[i]-1) + n])%mod;
				else if(s[i]=='?'&&t[i]!='?') g[i][j+n] = ( g[i+1][j - (0-b[i]) + n] + g[i+1][j - (1-b[i]) + n])%mod;
				else g[i][j+n] = ( ( g[i+1][j-1+n] + 2*g[i+1][j+n] ) % mod + g[i+1][j+1+n] )%mod;
			}
		}
		int ans=0;
		for(int i=1;i<=n;i++){
			for(int j=-n;j<=n;j++){
				(ans+=f[i][j+n]*g[i+1][-j+n]%mod*abs(j))%=mod;
			} 
		}
		printf("%lld\n",ans);
	}
	return 0;
}

6.CF1430G Yet Another DAG Problem

因為 \(B_i>0\) 所以每條邊 \(u \to v\), \(u\) 的權值大於 \(v\) 的權值
考慮給圖分層,每一層的點的權值相同,層數越大的點點權越小,相鄰層的權值差一定是\(1\) ,所以 \(u\) 的層數一定比 \(v\)

考慮狀壓 DP ,假設我們現在把點集 \(S\) 放到了前若干層,現在考慮下一層,假設下一層放的點集為\(T\),\(T\)需要滿足:

  1. \(T \operatorname{and} S=0\),即 \(T\)\(S\) 無交,相當於 \(T\)\(S\) 補集的子集
  2. 任意一個在 \(T\) 中的 \(v\),所有指向他的 \(u\) 必須都在 \(S\) 裡 (有了這個限制那也就必然滿足所有 \(v\) 指向的點都不在 \(S\) 中)

考慮轉移的代價:
對於每一個在 \(S\) 中的 \(u\) ,如果 \(u \to v\)\(v\) 不在 \(S\) 中,這條邊就會產生貢獻
假設 \(u\) 在第 \(x\) 層,\(v\) 在第 \(y\) 層,那這條邊的貢獻應該是 \(w_i \times (y-x)\)
但是這樣不是很好統計,所以我們考慮拆分貢獻:
因為 \(v\) 至少在下一層,所以每次轉移時我們把貢獻加上:
\(\sum w_i\)
即所有滿足 \(u\) 屬於 \(S\) , \(v\) 不屬於 \(S\) 的邊 \((u,v)\) 的邊權
這樣的話如果在下一次轉移時,這條邊的 \(v\) 依舊沒有被加入進來,那這條邊又會產生一次 \(w_i\) 的貢獻,直到 \(v\) 被加入進來,此時剛好產生 \(w_i \times (y-x)\) 的貢獻
我們對每一個S預處理出:

  • \(\sum wi \text{ (u,v)滿足u屬於S,v不屬於S}\)
  • 所有指向 \(S\) 中的點的點集
    預處理複雜度\(O(n \times n \times 2^n)\),DP列舉 \(T\) 的時間複雜度 \(O(3^n)\)

code

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5,M=(1<<18)+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m;
int tot,head[N],to[N],val[N],Next[N];
void add(int u,int v,int w){
	to[++tot]=v,Next[tot]=head[u],val[tot]=w,head[u]=tot;
} 
vector<int> G[N];
int sum[M],ru[M],f[M],from[M],ans[N];
void solve(int s,int val){
	int tmp=s^from[s];
	for(int u=1;u<=n;u++)
		if((s>>(u-1))&1) ans[u]=val;
	if(from[s]) solve(from[s],val+1);
}
void print(int s){
	for(int i=1;i<=n;i++){
		if(s>>(i-1)&1) cout<<i<<' ';
	}
}
signed main(){
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	n=read(),m=read();
	for(int i=1;i<=m;i++){
		int u=read(),v=read(),w=read();
		add(u,v,w);
		G[v].push_back(u);  //記錄入邊 
	}
	for(int s=0;s<(1<<n);s++){
		for(int u=1;u<=n;u++){
			if((s>>(u-1))&1){
				for(int i=head[u];i;i=Next[i]){
					int v=to[i],w=val[i];
					if(!((s>>(v-1))&1)) sum[s]+=w;
				}
				for(int v:G[u]){
					ru[s]|=(1<<(v-1));
				}
			}
		}
	}
	memset(f,0x3f,sizeof f);
	f[0]=0;
	for(int s=0;s<(1<<n);s++){
		int S=s^((1<<n)-1); //計算補集 
		for(int t=S;t;t=(t-1)&S){ //列舉t 
			if((ru[t]&s)==ru[t]){ //任意一個在T中的v,所有指向他的u必須都在S裡
				if(f[s]+sum[s]<f[t|s])
					f[t|s]=f[s]+sum[s],from[t|s]=s;
			}
		}
	} 
	solve((1<<n)-1,0);
	for(int i=1;i<=n;i++) printf("%d ",ans[i]);
	puts("");
	return 0;
}

7.CF1648D Serious Business

\(pre_{1/2/3}\)記錄每一行的字首和
\(f[x]\) 表示從 \((1,1)\)\((2,x)\) 的最大價值,
\(ans=\max(f[x]+pre_3[n]-pre_3[x-1])\)

考慮每一個操作 \((l_i,r_i,k_i)\)\(f[x]\) 的貢獻
顯然對於每一個 \(l_i \le y \le x\) 我都可以按以下路徑走: \((1,1) \to (1,y) \to (2,y) \to (2,x)\)

\[f[x]=max({pre_1[y]+pre_2[x]-pre_2[y-1]-k_i})=max({pre_1[y]-pre_2[y-1]-k_i})+pre_2[x] \]

對於每一個 \(l_i-1 \le y \le x\) 我也可以按照以下路徑走:
\((1,1) \to (2,y) \to (2,x)\)

\[f[x]=max({f[y]+pre_2[x]-pre_2[y]-k_i})=max({f[y]-pre_2[y]-k_i})+pre_2[x] \]

注意:這裡 \(a[2][y]\)\(f[y]\) 中算過了,所以是 \(pre_2[y]\) ,不是 \(pre_2[y-1]\)

對於第二個轉移還可以精簡:
如果 \(l_i \le y \le x\) , 那路徑必然是:
\((1,1) \to (1,z) \to (2,z) \to (2,y) \to (2,x)\)
其中 \(1 \le z \le y\)

  • 如果 \(li \le z \le y\),這條路徑
    \((1,1) -> (1,z) -> (2,z) -> (2,x)\)
    在轉移1中已經轉移過了
  • 如果 \(z<l_i\),這條路徑再細分:
    \((1,1) \to (1,z) \to (2,z) \to (2,l_i-1) \to (2,x)\)
    而這個其實就是 \(f[l_i-1]+pre_2[x]-pre_2[l_i-1-1]-k_i\)

所以轉移二其實只需要把 \(f[x]\)\(f[l_i-1]+pre_2[x]-pre_2[l_i-1]-k_i\)\(max\)

綜上得到轉移方程為:

\[ f[x]=\max( f[l_i-1] - pre2[l_i-1] - k_i , \max({pre_1[y] - pre_2[y-1] - k_i}) ) + pre_2[x] (l_i \le y \le x) \]

顯然可以先轉移後半部分,再轉移前半部分,並且前半部分的轉移可以直接線段樹區間取 \(max\)

現在討論後半部分的轉移
後半部分的轉移需滿足 \(l_i \le y \le x \le r_i\) , 很討厭 ,想辦法搞掉一個 \(r_i\)
可以從後往前列舉 \(x\) , 並不斷加入區間,這樣就滿足了\(r_ i\) 的條件 (但不一定滿足 \(l_i\) )
線段樹上維護三個值 \(max_A\) , \(max_B\) , \(max_{(A+B)}\)

  • \(max_A\):表示對應區間上最大的 \(-k_i\)
  • \(max_B\):表示對應區間上最大的 \(pre_1[y] - pre_2[y-1]\)
  • \(max_{(A+B)}\):表示對應區間上滿足的最大的 \(-k_i + pre_1[y] - pre_2[y-1]\) (並且需要滿足 \(-k_i\) 對應的\(l_i\)\(pre_1[y] - pre_2[y-1]\) 對應的 \(y\) 之前)

於是只要在區間 \([1,x]\) 上查詢 \(max_{(A+B)}\)即可

對於 \(-k_i\) 可以在往前掃的時候插入,對於 \(pre_1[y] - pre_2[y-1]\) 可以提前就插入
線段樹合併時就先合併 \(max_A\)\(max_B\)
對於 \(max_{(A+B)}\) : 先繼承左右兒子的 \(max_{(A+B)}\),再用左兒子的 {max_A} 加上 右兒子的 {max_B}

兩部分轉移各需要一棵線段樹

\({\color{green} {插一嘴,真的難寫}}\)

code

#include<bits/stdc++.h>
#define int long long
#define PIII pair<pair<int,int>,int>
#define fi first
#define se second
using namespace std;
const int N=5e5+5,inf=0x3f3f3f3f3f3f3f3f;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,q,a[5][N],pre[5][N],f[N];
struct P{
	int l,r,k;
}b[N];
struct node1{
	int l,r,max_A,max_B,max_sum;
};
struct SegmentTree1{
	node1 t[N<<2];
	void pushup(int p){
		t[p].max_A=max(t[p<<1].max_A,t[p<<1|1].max_A);
		t[p].max_B=max(t[p<<1].max_B,t[p<<1|1].max_B);
		t[p].max_sum=max({t[p<<1].max_sum,t[p<<1|1].max_sum,t[p<<1].max_A+t[p<<1|1].max_B});
	}
	void build(int p,int l,int r){
		t[p].l=l,t[p].r=r;
		if(l==r){
			t[p].max_A=t[p].max_B=t[p].max_sum=-inf;
			return;
		}
		int mid=(t[p].l+t[p].r)>>1;
		build(p<<1,l,mid);
		build(p<<1|1,mid+1,r);
		pushup(p);
	}
	void change_A(int p,int x,int val){
		if(t[p].l==t[p].r){
			t[p].max_A=max(t[p].max_A,val);
			t[p].max_sum=max(t[p].max_sum,t[p].max_A+t[p].max_B);
			return;
		}
		int mid=(t[p].l+t[p].r)>>1;
		if(x<=mid) change_A(p<<1,x,val);
		else change_A(p<<1|1,x,val);
		pushup(p);
	}
	void change_B(int p,int x,int val){
		if(t[p].l==t[p].r){
			t[p].max_B=max(t[p].max_B,val);
			t[p].max_sum=max(t[p].max_sum,t[p].max_A+t[p].max_B);
			return;
		}
		int mid=(t[p].l+t[p].r)>>1;
		if(x<=mid) change_B(p<<1,x,val);
		else change_B(p<<1|1,x,val);
		pushup(p);
	}
	PIII ask(int p,int l,int r){
		if(l<=t[p].l&&t[p].r<=r) return {{t[p].max_A,t[p].max_B},t[p].max_sum};
		int mid=(t[p].l+t[p].r)>>1;
		if(r<=mid) return ask(p<<1,l,r);
		if(l>mid) return ask(p<<1|1,l,r);
		PIII res1=ask(p<<1,l,r),res2=ask(p<<1|1,l,r);
		int max_A=max(res1.fi.fi,res2.fi.fi),max_B=max(res1.fi.se,res2.fi.se),max_sum=max({res1.se,res2.se,res1.fi.fi+res2.fi.se});
		return {{max_A,max_B},max_sum};
	}
}T1;
struct node2{
	int l,r,maxn,lazy;
	void tag(int val){
		maxn=max(maxn,val);
		lazy=max(lazy,val);
	}
};
struct SegmentTree2{
	node2 t[N<<2];
	void pushup(int p){
		t[p].maxn=max(t[p<<1].maxn,t[p<<1|1].maxn);
	}
	void spread(int p){
		if(t[p].lazy!=-inf){
			t[p<<1].tag(t[p].lazy);
			t[p<<1|1].tag(t[p].lazy);
			t[p].lazy=-inf;
		}
	}
	void build(int p,int l,int r){
		t[p].l=l,t[p].r=r,t[p].lazy=-inf;
		if(l==r){
			t[p].maxn=f[l];
			return;
		}
		int mid=(t[p].l+t[p].r)>>1;
		build(p<<1,l,mid);
		build(p<<1|1,mid+1,r);
		pushup(p); 
	}
	void change(int p,int l,int r,int val){
		if(l<=t[p].l&&t[p].r<=r){
			t[p].tag(val);
			return;
		}
		spread(p);
		int mid=(t[p].l+t[p].r)>>1;
		if(l<=mid) change(p<<1,l,r,val);
		if(r>mid) change(p<<1|1,l,r,val);
		pushup(p);
	}
	int ask(int p,int x){
		if(t[p].l==t[p].r) return t[p].maxn;
		spread(p);
		int mid=(t[p].l+t[p].r)>>1;
		if(x<=mid) return ask(p<<1,x);
		else return ask(p<<1|1,x);
	}
}T2;
signed main(){
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	n=read(),q=read();
	for(int i=1;i<=3;i++){
		for(int j=1;j<=n;j++){
			a[i][j]=read();
			pre[i][j]=pre[i][j-1]+a[i][j];
		}
	}
	for(int i=1;i<=q;i++) b[i].l=read(),b[i].r=read(),b[i].k=read();
	
	T1.build(1,1,n);
	for(int i=1;i<=n;i++)
		T1.change_B(1,i,pre[1][i] - pre[2][i-1]);
	sort(b+1,b+q+1,[](const P&x,const P&y){return x.r>y.r;});
	int i=1;
	for(int x=n;x>=1;x--){
		while(i<=q&&b[i].r>=x) T1.change_A(1,b[i].l,-b[i].k),i++;
		f[x]=T1.ask(1,1,x).se; //先別加pre[2][x] 
	}
	
	T2.build(1,1,n);   //建樹的時候直接按照f陣列 
	
	sort(b+1,b+q+1,[](const P&x,const P&y){return x.l<y.l;});
	for(int i=1;i<=q;i++){
		int l=b[i].l,r=b[i].r;
		if(l>1){
			int tmp=T2.ask(1,l-1);
			T2.change(1,l,r,tmp-b[i].k);
			//注意這個時候因為我們的終點並不是(2,l-1),f[l-1]所以不能加上pre[2][l-1],直接用原來沒有加過pre2的f值是對的 
		}
		//這個轉移的意義是先走到(2,l-1),所以l!=1 
	}
	for(int i=1;i<=n;i++) f[i]=T2.ask(1,i)+pre[2][i];   
	int ans=-inf;
	for(int i=1;i<=n;i++){
		ans=max(ans,f[i]+pre[3][n]-pre[3][i-1]);
	}
	printf("%lld\n",ans);
	return 0;
}

8.CF367E Sereja and Intervals

一個關於互不包含區間的結論:
如果把區間左端點升序排序,則區間右端點也必然升序
也就是說當你確認了 \(n\)\(l_i\) ,和 \(n\)\(r_i\),那他們的組合方法是唯一的.
所以你現在要構造兩個序列 {\(l_i\)},{\(r_i\)} ,滿足:

  • \(1 \le l_1 < l_2 < l_3 < l_4 < l_5 < ...< l_n \le m\)
  • $ 1 \le r_1 < r_2 < r_3 < r_4 < r_5 < ... < r_n<=m $
  • \(l_i \le r_i\)
  • \(\text{存在} l_i=x\)

考慮幾個性質:

  • 不可能存在一個點存在兩個左端點/右端點
  • \(n>m\) 時無解,所以可以認為 \(n \le m\) , 即 \(n \le \sqrt {10^5}\)
  • 3限制等價於對於每一個\(i\),其左邊左端點 \(cnt(l,i)\) 的數量 \(\ge\) 其左邊右端點的數量 \(cnt(r,i)\)

如果沒有限制4,考慮 DP :
\(f[i][j][k]\) 表示給前 \(i\) 個位置分配 \(j\)\(l\) 端點,\(k\)\(r\) 端點的方案數 \((j \ge k)\)
轉移時,每一個位置你要麼分配一個 \(l\) ,要麼分配一個 \(r\) ,要麼分配 \(l\) , \(r\) 各一個,要麼啥也不分配
時間複雜度\(O(m \times n^2)\)

如果有限制4,其實只需要在 \(i=x\) 時,強制只轉移有放左端點的情況

code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e3+5,mod=1e9+7;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,x;
int f[2][400][400];   //滾動陣列 
signed main(){
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	n=read(),m=read(),x=read();
	if(n>m){
		printf("0\n");
		return 0;
	}
	f[0][0][0]=1;
	for(int i=1;i<=m;i++){
		for(int j=0;j<=min(i,n);j++){
			for(int k=0;k<=j;k++){
				if(j-1>=k&&j-1>=0) (f[i&1][j][k]+=f[1-(i&1)][j-1][k])%=mod;
				if(i!=x&&k-1>=0) (f[i&1][j][k]+=f[1-(i&1)][j][k-1])%=mod;
				if(j-1>=0&&k-1>=0) (f[i&1][j][k]+=f[1-(i&1)][j-1][k-1])%=mod;
				if(i!=x) (f[i&1][j][k]+=f[1-(i&1)][j][k])%=mod;
			}
		}
		for(int j=0;j<=min(n,i);j++){
			for(int k=0;k<=j;k++){
				f[1-(i&1)][j][k]=0;
			}
		}
	}
	int fac=1;
	for(int i=1;i<=n;i++) (fac*=i)%=mod;   //區間有編號所以乘以n! 
	printf("%lld\n",f[m&1][n][n]*fac%mod);
	return 0;
}

9.CF1574F Occurrences

這個限制是真的抽象,而且這題裡的子序列指子串,為了避免誤會下面直接寫子串

考慮轉化限制:
仔細想一想可以發現 \(A\) 在序列 \(a\) 中每出現一次,
其每個子串都會出現一次,且出現次數最多的一定是長度為\(1\)的單個字元。
所以這個限制其實就是:\(A\)\(a\) 中出現的次數 \(=\) 其每個字元在 \(a\) 中出現的次數。

考慮幾種情況:

  1. 如果\(A=\text{123}\),此時當 \(a\) 中填了一個 1 時,必定要按照順訊再填進去 2,3\(a\) 中要麼沒有 1 ,有的話一定是以 123 的形式出現的。
    如果連邊的話會發現此時 \(1 \to 2 \to 3\)構成一條鏈。
  2. 如果\(A=\text{121}\),會發現此時無論如何填1出現的次數一定大於 \(A\),除非不填1
    而此時連邊會出現環,即有環的一定不能填。
  3. 如果\(A=\text{123},B=\text{234}\),此時要把 \(A\)\(B\) 的鏈合併,變成1234
  4. 如果\(A=\text{123},B=\text{124}\),此時出現了分支,同樣不能填。

於是一個做法就成型了:

  1. 一開始每個數字自成一個連通塊。
  2. 對每個序列 \(A\) ,按照順序連邊。
  3. 對每一個連通塊,如果它存在環或者存在分支(即它不是鏈),那這個連通塊內的數都不能出現在 \(a\) 中。
    否則這條鏈可以出現在a中
  4. 對每一個可以出現的鏈做一次完全揹包,即如果長度為 \(j\) 的鏈有 \(w_j\) 個,\(f[i]\) 表示構造長度為 \(i\)\(a\) 的方案數,有\(f[i]=\sum_{j=1}^i f[i-j] \times w_j\)

這樣轉移可以大大加快速度,因為列舉每個鏈可能有 \(O(k)\)個,但不同的鏈長一定小於等於 \(O(\sqrt {k})\)
因為假設不同鏈長有 \(l\) 個,那即使這些鏈的長度為\(1,2,3,4,...,l\)
\((1+2+3+..+l)=\frac {(1+l) \times l}{2} \le k\),即$ l \le \sqrt{2k}$

code

#include<bits/stdc++.h>
#define int long long
#define PII pair<int,int>
#define fi first
#define se second
using namespace std;
const int N=3e5+5,mod=998244353;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,k,a[N]; 
map<PII,bool> mp;
int tot,head[N],to[N],Next[N],ru[N],chu[N]; 
void add(int u,int v){
	to[++tot]=v,Next[tot]=head[u],head[u]=tot;
	ru[v]++,chu[u]++;
}
int num,c[N],siz[N];
bool vis[N],flag[N];
void dfs(int u){
	siz[num]++;
	vis[u]=true,c[u]=num;
	for(int i=head[u];i;i=Next[i]){
		int v=to[i];
		if(vis[v]) flag[num]=false;
		else dfs(v);
	}
}
set<int> len;
int w[N],f[N];
signed main(){
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	n=read(),m=read(),k=read();
	memset(flag,true,sizeof flag);
	for(int i=1;i<=n;i++){
		int c=read(),lst;
		for(int j=1;j<=c;j++){
			a[j]=read();
			if(j>1&&!mp[{lst,a[j]}]) add(lst,a[j]),mp[{lst,a[j]}]=true;;
			lst=a[j];
		}
	}
	for(int i=1;i<=k;i++){
		if(!vis[i]&&ru[i]==0) num++,dfs(i);
	}
	for(int i=1;i<=k;i++) 
		if(ru[i]>1||chu[i]>1) flag[c[i]]=false;
	for(int i=1;i<=num;i++)
		if(flag[i]) len.insert(siz[i]),w[siz[i]]++;

	f[0]=1;
	
	for(int i=1;i<=m;i++){
		for(int x:len) if(i>=x) (f[i]+=f[i-x]*w[x])%=mod;
	}
	printf("%lld\n",f[m]);
	return 0;
}


10.P5662 [CSP-J2019] 紀念品

因為當日購買的紀念品也可以當日賣出換回金幣。
所以如果想保留一個紀念品可以看成是:

第一天買,第二天早上賣,第二天再買回,第三天早上賣,第三天再買回...

這樣就不用管每一天手上有多少紀念品,只需要認為我當天買的第二天一早一定會直接賣掉,啥也不剩,至於再買不買回來是第二天的事 。

\(A_{i,j}\) 表示第 \(i\)\(j\) 物品的價格。
假設我考慮到了第 \(i\) 天,手裡剩 \(M\) 元 , 買入一個物品需要花 \(A_{i,j}\) 元,收益是 \(A_{i+1,j} - A_{i,j}\)
可以看成是有一個體積為 \(M\) 的揹包,每個物品的體積為 \(A_{i,j}\) ,價值是 \(A_{i+1,j} - A_{i,j}\) 的完全揹包
最後按照當天獲得的最大價值當做下一天的起始資金即可。

code

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int T,n,M,a[105][105];
int f[N];
signed main(){
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	T=read(),n=read(),M=read();
	for(int i=1;i<=T;i++){
		for(int j=1;j<=n;j++){
			a[i][j]=read();
		}
	}
	for(int i=1;i<T;i++){
		for(int k=0;k<=M;k++) f[k]=0;
		for(int j=1;j<=n;j++){
			for(int k=a[i][j];k<=M;k++){
				f[k]=max(f[k],f[k-a[i][j]]+a[i+1][j]-a[i][j]);
			}
		}
		M=M+f[M];  //因為f陣列算的是可以收益多少,所以M直接+f[M] 
	}
	printf("%d\n",M);
	return 0;
}

11.P2886 [USACO07NOV] Cow Relays G

矩陣快速冪最佳化 DP 板子

我們用一個矩陣表示兩兩之間的答案,一開始 \(A[i][j]\) 表示 \(i\) , \(j\) 只經過一條邊的最短路
把矩陣乘法改成 \(C[i][j] = min(A[i][k]+A[k][j])\)
這裡 k 相當於列舉了中轉點 (參考Floyd)
並且此時 路徑長度會變成2倍,最終要求是 n 倍,做矩陣快速冪即可

code

#include<bits/stdc++.h>
#define int long long 
#define PIII pair<int,pair<int,int> > 
#define fi first
#define se second
using namespace std;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int dis[10005],N,n,m,s,t;
vector<PIII> G;
int Dis(int x){
	return lower_bound(dis+1,dis+n+1,x)-dis;
}

int ans[1005][1005],c[1005][1005];
void mul(int f[1005][1005],int a[1005][1005]){  //矩陣乘法 
	memset(c,0x3f,sizeof c);
	for(int k=1;k<=n;k++){
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				c[i][j]=min(c[i][j],f[i][k]+a[k][j]);
			}
		}
	}
	memcpy(f,c,sizeof c);
}
void Quick_power(int a[1005][1005],int b){
	for(int i=1;i<=n;i++){    //單位矩陣 
		for(int j=1;j<=n;j++){
			if(i==j) ans[i][j]=0;
			else ans[i][j]=0x3f3f3f3f3f3f3f3f;
		}
	}
	while(b){
		if(b&1) mul(ans,a);
		b>>=1,mul(a,a);
	} 
	memcpy(a,ans,sizeof ans);
}
int a[1005][1005];
signed main(){
	N=read(),m=read(),s=read(),t=read();
	memset(a,0x3f,sizeof a);
	for(int i=1;i<=m;i++){
		int w=read(),u=read(),v=read();
		G.push_back({u,{v,w}});
		dis[i]=u,dis[i+m]=v;
	}
	sort(dis+1,dis+2*m+1);
	n=unique(dis+1,dis+2*m+1)-dis-1;
	for(PIII x:G){
		int u=x.fi,v=x.se.fi,w=x.se.se;
		u=Dis(u),v=Dis(v);
		a[u][v]=a[v][u]=w;
	}
	
	Quick_power(a,N);
	
	printf("%lld\n",a[Dis(s)][Dis(t)]);
	return 0;
}

12.P6569 [NOI Online #3 提高組] 魔法值

相同的轉移情況再次想到矩陣快速冪。

構造轉移矩陣 \(G\) , 若 \(u\),\(v\) 之間有邊,則 \(G[u][v]=1\) , 否則 \(G[u][v]=0\)
將矩陣乘法改成 \(C[i][j] = \operatorname{xor} ^ n _ {k=1} A[i][k] \times B[k][j]\)
注意:廣義矩乘若要滿足結合律必須滿足——加法滿足交換律,乘法滿足結合律,並對加法滿足分配率,而普通的異或對加法是沒有分配律的,也不能這麼改變,但由於這裡只有 \(0/1\) 所以可以

直接快速冪的總時間複雜度是 $ O(q \times n^3 * \log a)$ 過不了。
所以考慮預處理出 \(G^1,G^2,G^4,......\) ,並對 \(a\) 進行二進位制拆分,這樣每一次乘都是一個 \(n \times n\)矩陣乘以 \(1 \times n\)的向量,時間複雜度變成 $ O(q \times n^2 * \log a)$

code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,q; 
struct matrix{
	int x[105][105];
	int n,m;
}f,a,mi[40];
matrix mul(matrix x,matrix y){
	matrix ans;
	ans.n=x.n,ans.m=y.m;
	memset(ans.x,0,sizeof ans.x);
	for(int k=1;k<=x.m;k++){
		for(int i=1;i<=ans.n;i++){
			for(int j=1;j<=ans.m;j++){
				ans.x[i][j]^=(x.x[i][k]*y.x[k][j]);
			}
		}
	}
	
	return ans;
}
signed main(){
	n=read(),m=read(),q=read();
	f.n=1,f.m=n;
	a.n=n,a.m=n;
	for(int i=1;i<=n;i++) f.x[1][i]=read();
	for(int i=1;i<=m;i++){
		int u=read(),v=read();
		a.x[u][v]=a.x[v][u]=1;
	}
	
	mi[0]=a;
	for(int i=1;i<=32;i++) mi[i]=mul(mi[i-1],mi[i-1]);
	
	
	while(q--){
		int t=read();
		matrix ans=f;
		for(int i=0;i<=32;i++){
			if(t>>i&1) ans=mul(ans,mi[i]);
		}
		printf("%lld\n",ans.x[1][1]);
	}
	return 0;
}

13.P6190 [NOI Online #1 入門組] 魔法

一條合法的 \(1 \to n\) 路徑可以拆成兩部分:

  1. 一開始沒有任何魔法的路徑
  2. 若干段滿足:第一條路徑用了魔法,後面沒有用的路徑

並且如果可以用完一定會把 \(k\) 次魔法用完。
所以 \(A[i][j]\) 表示 \(i\)\(j\) 用一次魔法且用在第一條的最短距離。
矩陣快速冪到 \(A^k\) 即為用了 \(k\) 次魔法。
注意還要用Floyd跑出全源最短路,處理出那些沒有任何魔法的路徑的最小值。

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e5+5,inf=1e15;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,k;
void Init(int a[105][105]){
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			if(i!=j) a[i][j]=inf;   //這裡特殊判斷 i!=j , 防止 1--n 路徑上可能不足 k 條邊的情況 
		}
	}
}
int ans[105][105],c[105][105];
void mul(int f[105][105],int a[105][105]){  //矩陣乘法 
	Init(c);
	for(int k=1;k<=n;k++){
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				c[i][j]=min(c[i][j],f[i][k]+a[k][j]);	
			}
		}
	}
	memcpy(f,c,sizeof c);
}
void Quick_power(int a[105][105],int b){
	for(int i=1;i<=n;i++){    
		for(int j=1;j<=n;j++){
			if(i==j) ans[i][j]=0;
			else ans[i][j]=inf;
		}
	}
	while(b){
		if(b&1) mul(ans,a);
		b>>=1,mul(a,a);
	} 
	memcpy(a,ans,sizeof ans);
}

int f[105][105],a[105][105]; 
int tot,head[N],to[N],Next[N],val[N];
void add(int u,int v,int w){
	to[++tot]=v,Next[tot]=head[u],val[tot]=w,head[u]=tot;
}
signed main(){
	n=read(),m=read(),k=read();
	
	Init(f);
	for(int i=1;i<=n;i++) f[i][i]=0;
	for(int i=1;i<=m;i++){
		int u=read(),v=read(),w=read();
		f[u][v]=w;
		add(u,v,w);
	}
	for(int k=1;k<=n;k++){
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
				
			}
		}
	}
	
	if(k==0){
		printf("%lld\n",f[1][n]);
		return 0;
	}
	
	Init(a);
	for(int u=1;u<=n;u++){
		for(int v=1;v<=n;v++){
			for(int i=head[u];i;i=Next[i]){
				int w=val[i];
				a[u][v]=min(a[u][v],-w+f[to[i]][v]);
			}
		}
	}
	
	Quick_power(a,k);

	int res=inf;
	for(int u=1;u<=n;u++){
		res=min(res,f[1][u]+a[u][n]);
	}
	printf("%lld\n",res);
	return 0;
}

14.P6772 [NOI2020] 美食家

樸素 DP : \(f[i][u]\) 表示第 \(i\) 天到 \(u\) 的最大收益。
\(f[i][u] = max(f[i-w][v]+c[u])\) (存在一條邊 \((v,u,w)\) )

因為 \(T\) 很大,考慮矩陣快速冪最佳化:
由於矩陣快速冪一般只能最佳化從 \(f[i] \to f[i+1]\) 的轉移,所以考慮拆點
Tip:之所以不拆邊是因為 n 比較小
即把一個點 \(u\) 拆成 $ u_1,u_2,u_3,u_4,u_5$
並且按照 \(u_1 \to u_2 \to u_3 \to u_4 \to u_5\) 連邊
若存在一條邊 \((u,v,3)\) 則按照 \(u_3 -> v_1\) 連邊
相當於變成新圖中經過了多少條邊就是幾天。
並且只在所有節點分裂出的第一個點,即 \(u_1\) 上 $c[ u_1 ]= c[u] $, 其餘點的 \(c\) 值均為 \(0\)

設計轉移矩陣 \(G\) ,若新圖中 \(u\)\(v\) 之間有邊,
\(G[u][v] = c[v]\),否則 \(G[u][v] = -inf\)
改變矩乘定義:
$ C[i][j] = \max{A[i][k] + B[k][j]} $。

一開始除了 \(f[1][1]=c[1]\) 其餘均為 \(-inf\)
若不考慮美食節 , 則 答案 \(=Ans[1][1]\) ,其中 \(Ans=f \times G^T\)

對於有美食節的情況:
在每個美食節做一次樸素轉移,具體來說
因為每個美食節不在一個一個時間舉行,先按照每個美食節的時間排序
對於 \(t[i-1]\)\(t[i]\) :
\(f = f * G ^ {t[i] - t[i-1]}\)
之後再特殊將 \(f[1][x[i] ] = f[1][x[i]] + y[i]\)
若最後 \(t[k] \ne T\) 則再將 \(f = f * G ^ (T-t[k])\)
但是時間複雜度為 \(O(k \times (5n) ^ 3 \times \log T)\)

為了最佳化複雜度,我們還是考慮二進位制拆分最佳化,參見 P6569 [NOI Online #3 提高組] 魔法值
將所有 \(G^1,G^2,G^4 ....\) 預處理出來,預處理時間複雜度 \(O((5n) ^ 3 * \log T)\)
每一次二進位制拆分 \(t[i]-t[i-1]\) , 依次乘上對應的矩陣。
由於這裡每一次乘都是一個 \(1 \times 5n\) 的向量 \(f\) 乘上一個 \(5n \times 5n\) 的矩陣 \(G\) , 時間複雜度只有 \(O(k * (5n) ^ 2 \times logT)\)

總時間複雜度 \(O((5n) ^ 3 \times \log T + k \times (5n) ^ 2 \times \log T)\)

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e5+5,inf=5e15;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,T,k;
int c[N];
struct Festival{
	int t,x,y;
}a[N];
bool edge[300][300];
void add(int u,int v){
	edge[u][v]=true;
}

struct Matrix{
	int a[300][300];
	int n,m;
	void Init(int val){
		for(int i=1;i<=n;i++){
			for(int j=1;j<=m;j++){
				a[i][j]=val;
			}
		}
	}
}G,F,mi[32];
Matrix operator * (const Matrix &A,const Matrix &B){
	Matrix C;
	C.n=A.n,C.m=B.m;
	C.Init(-inf); 
	for(int k=1;k<=A.m;k++){
		for(int i=1;i<=C.n;i++){
			for(int j=1;j<=C.m;j++){
				C.a[i][j]=max(C.a[i][j],A.a[i][k]+B.a[k][j]);
			}
		}
	}
	return C;
}
signed main(){
	n=read(),m=read(),T=read(),k=read();
	for(int i=1;i<=n;i++){
		c[i]=read();
		for(int j=1;j<=4;j++){
			add(i+(j-1)*n,i+j*n);
		}
	}
	for(int i=1;i<=m;i++){
		int u=read(),v=read(),w=read();
		add(u+(w-1)*n,v);
	}
	for(int i=1;i<=k;i++){
		a[i].t=read(),a[i].x=read(),a[i].y=read();
	}
	
	F.n=1,F.m=5*n;
	G.n=5*n,G.m=5*n;
	F.Init(-inf);
	F.a[1][1]=c[1];
	for(int i=1;i<=5*n;i++){
		for(int j=1;j<=5*n;j++){
			if(edge[i][j]) G.a[i][j]=c[j];
			else G.a[i][j]=-inf;
		}
	}
	mi[0]=G;
	for(int i=1;i<=30;i++) mi[i]=mi[i-1]*mi[i-1]; 
	
	sort(a+1,a+k+1,[](Festival x,Festival y){return x.t<y.t;});
	a[0].t=0;
	for(int i=1;i<=k;i++){
		int t=a[i].t-a[i-1].t;
		for(int j=0;j<=30;j++)
			if(t>>j&1) F=F*mi[j];
		F.a[1][a[i].x]+=a[i].y;
	}
	if(a[k].t!=T){
		int t=T-a[k].t;
		for(int j=0;j<=30;j++)
			if(t>>j&1) F=F*mi[j];
	}
	
	if(F.a[1][1]<0) printf("-1\n");
	else printf("%lld\n",F.a[1][1]);
	return 0;
}

15.CF1051E Vasya and Big Integers

\(f[i]\) 表示劃分後 \([i,n]\) 的方案。
能從 \(f[j]\) 轉移到 \(f[i]\)\(j\) 一定在 \([i+lenL,i+lenR]\) 中的一段連續區間 ( \(lenR\)\(r\) 的長度,\(lenL\)\(l\) 的長度)。
事實上只有 \(j=i+lenR\)\(j=i+LenL\) 時有可能不合法,特殊判斷一下(二分+ hash 或 ex_KMP) 即可。
轉移時用字尾和最佳化。
之所以這麼設計狀態是因為這樣基本不用考慮前導零。

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e6+5,mod=998244353;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
string s,a,b;
int n,lenL,lenR;
int z[N],p1[N],p2[N];
int f[N],q[N];
void Init(){
	int l=0,r=0;
	z[1]=lenL;
	for(int i=2;i<=lenL;i++){
		if(i<=r) z[i]=min(r-i+1,z[i-l+1]);
		while(a[1+z[i]]==a[i+z[i]]) z[i]++;
		if(i+z[i]-1>r) l=i,r=i+z[i]-1;
	}
	l=0,r=0;
	for(int i=1;i<=n;i++){
		if(i<=r) p1[i]=min(r-i+1,z[i-l+1]);
		while(1+p1[i]<=lenL&&i+p1[i]<=n&&s[i+p1[i]]==a[1+p1[i]]) p1[i]++;
		if(i+p1[i]-1>r) l=i,r=i+p1[i]-1;
	}
	
	memset(z,0,sizeof z);    //清空!!!!!! 
	l=0,r=0;
	z[1]=lenR;
	for(int i=2;i<=lenR;i++){
		if(i<=r) z[i]=min(r-i+1,z[i-l+1]);
		while(b[1+z[i]]==b[i+z[i]]) z[i]++;
		if(i+z[i]-1>r) l=i,r=i+z[i]-1;
	}
	l=0,r=0;
	for(int i=1;i<=n;i++){
		if(i<=r) p2[i]=min(r-i+1,z[i-l+1]);
		while(1+p2[i]<=lenR&&i+p2[i]<=n&&s[i+p2[i]]==b[1+p2[i]]) p2[i]++;
		if(i+p2[i]-1>r) l=i,r=i+p2[i]-1;
	}
}
bool cmp1(int l,int r){
	if(r-l+1<lenL) return false;
	if(r-l+1>lenL) return true;
	if(p1[l]==lenL) return true;
	return a[1+p1[l]]<=s[l+p1[l]];
}
bool cmp2(int l,int r){
	if(r-l+1<lenR) return true;
	if(r-l+1>lenR) return false;
	if(p2[l]==lenR) return true;
	return b[1+p2[l]]>=s[l+p2[l]];	
}
bool check(int l,int r){
	if(l>r) return false;
	return cmp1(l,r)&&cmp2(l,r); 
}
signed main(){
//	freopen("cyq.in","r",stdin);
//	freopen("cyq.out","w",stdout);
	cin>>s>>a>>b;
	n=s.size(),lenL=a.size(),lenR=b.size();
	s=' '+s,a=' '+a,b=' '+b;
	Init();
	
	f[n+1]=1;
	q[n+1]=1;
	for(int i=n;i>=1;i--){
		if(s[i]=='0'){
			if(check(i,i)) f[i]=f[i+1];
		}
		else{
			int l=min(n+1,i+lenL),r=min(n+1,i+lenR);
			if(!check(i,l-1)) l++;
			if(!check(i,r-1)) r--;
			if(l<=r) f[i]=(q[l]-q[r+1]+mod)%mod;
			else f[i]=0;
		}
		q[i]=(q[i+1]+f[i])%mod;
	}
	printf("%lld\n",f[1]);
	return 0;
}

16.CF1310C Au Pont Rouge

所有子串排序後二分答案。
check 相當於要求把 \(S\) 分割成 \(m\) 段大小都大於一個給定子串的方案數,轉移和上題類似 。

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e6+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int  n,m,cnt,k;
string s;
int lcp[1005][1005];
struct P{
	int l,r;
}a[N];
bool operator > (P const &x, P const &y) {
	int L=lcp[x.l][y.l];
	if (L>=x.r-x.l+1||L>=y.r-y.l+1)
		return x.r-x.l+1>y.r-y.l+1;
	return s[x.l+L]>s[y.l+L];
}
int f[1005][1005],q[1005][1005];
// f[j][i] 表示把 [i,n] 劃分成 j 使每一段都比給定的大,q[j][i]表示對於 j 的字尾和。 
int check(P x){
	memset(f,0,sizeof f);
	memset(q,0,sizeof q);
	f[0][n+1]=1;
	for(int i=n+1;i>=1;i--) q[0][i]=q[0][i+1]+f[0][i];
	int l=x.l,r=x.r,len=r-l+1;
	for(int j=1;j<=m;j++){
		for(int i=n;i>=1;i--){
			int tmp=min(len,lcp[i][l]);
			if(tmp==len||s[i+tmp]>=s[l+tmp]) 
				f[j][i]=q[j-1][i+tmp+1];
		}
		for(int i=n;i>=1;i--) q[j][i]=min((long long)1e18,q[j][i+1]+f[j][i]);   //防止爆掉 
	}
	return f[m][1]; 
}
signed main(){
	n=read(),m=read(),k=read();
	cin>>s; s=' '+s;
	lcp[n][n]=1;
	for(int i=n;i>=1;i--){
		for(int j=n;j>=1;j--){
			if(i==j&&i==n) continue;
			if(s[i]==s[j]) lcp[i][j]=lcp[i+1][j+1]+1;
			else lcp[i][j]=0;
		}
	}
	for(int i=1;i<=n;i++){
		for(int j=i;j<=n;j++){
			a[++cnt]={i,j};
		}
	}
	
	sort(a+1,a+cnt+1,greater<P>());
	
	int l=1,r=cnt,mid,ans;
	while(l<=r){
		mid=(l+r)>>1;
		if(check(a[mid])+1<=k) ans=mid,l=mid+1;
		else r=mid-1;
	}
	for(int i=a[ans].l;i<=a[ans].r;i++){
		printf("%c",s[i]);
	}
	printf("\n");
	return 0;
}

17.CF477D Dreamoon and Binary

  • 方案數:
    一個很好想的 DP , 設 \(f[i][j]\) 表示最後一段為 \([j,i]\) , 劃分 \([1,i]\) 的方案數。
    \(f[i][j]=\sum_{k=j-1-len+1}^{j-1} f[j-1][k]\) ,其中 $ len=i-j+1$ 。
    轉移用字首和最佳化即可做到 \(O(n^2)\)
    注意當 \(k=j-1-len+1\)\(s[k,j-1]\)\(s[j,i]\) 長度一樣時要判斷前一個是否比後一個小 , 可以預處理 LCP
    對於前導\(0\)的處理只需要在\(0\)的位置不算入字首和即可。

  • 最小操作次數:
    乍一看取模之後似乎是不能比較大小的,所以就不能 DP ,所以分析性質。
    考慮最後的答案是怎麼算的:
    \(ans = \text{列印次數+加一次數} = m+val\)
    其中 \(m\) 為段數, \(val\) 為最後一段的值。
    假設最後一段為 \(s[i,n]\)
    \(i\) 前移時, \(m\) 只會最多減少 \(1\) , 而 \(val\) 會多一個數量級,而 \(m \le 5000 < 2^{17}\)
    所以我們其實只需要考慮 \(n-16 \le i\le n\) 的答案即可,如果這個位置的劃分不合法就是第一問中 f[n][i]=0 。
    我們只需要 DP 求出最小的段數即可,這裡由於 \(j \le i\) ,所以 DP 時記錄一下字尾最小值 , 就可以也做到 \(O(n^2)\)
    Tip:當然如果這個區間裡無解還要繼續往前。(見程式碼)

define int long long ,卡空間

code

#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N=1e5+5,inf=0x3f3f3f3f;
const ll mod=1e9+7;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
string s;
int n; 
int lcp[5005][5005];
void Init(){
	for(int i=n;i>=1;i--){
		for(int j=n;j>=1;j--){
			if(s[i]==s[j]) lcp[i][j]=lcp[i+1][j+1]+1;
			else lcp[i][j]=0;
		}
	}
}
bool cmp(int l,int r,int x,int y){   //判斷s[l,r]是否<=s[x,y] 
	int len1=r-l+1,len2=y-x+1;
	if(len1<len2) return true;
	if(len1>len2) return false;
	if(lcp[l][x]>=len1) return true;
	return s[l+lcp[l][x]]<=s[x+lcp[l][x]];
}
int f[5005][5005],q[5005][5005];
int g[5005][5005],ming[5005][5005]; //最小段數,g陣列的字尾最小值 
void solve1(){
	memset(g,0x3f,sizeof g); 
	memset(ming,0x3f,sizeof ming); 
	for(int i=1;i<=n;i++){
		for(int j=1;j<=i;j++){
			if(i!=j&&s[j]=='0'){
				f[i][j]=0;
				g[i][j]=inf;
			}
			else if(j==1){
				f[i][j]=1;
				g[i][j]=1;
			} 
			else{
				int k=j-1-(i-j+1)+1; k=max(k,1);
				if(!cmp(k,j-1,j,i)) k++;
				f[i][j]=((ll)(q[j-1][j-1]+mod-q[j-1][k-1]))%mod;
				g[i][j]=ming[j-1][k]+1;
			}
			q[i][j]=((ll)(q[i][j-1]+f[i][j]))%mod;
		}
		for(int j=i;j>=1;j--) ming[i][j]=min(ming[i][j+1],g[i][j]);
	}
	printf("%d\n",q[n][n]);
}
void solve2(){
	ll mi=1,val=0,ans=inf;
	bool flag=false;  //記錄是否出現瞭解,如果 i 在 [n-16,n] 之內沒有解要繼續往前 
	for(int i=n;i>=max(1,n-16);i--){
		val=val+(ll)(s[i]-'0')*mi; mi*=2ll;
		if(f[n][i]!=0) flag=true,ans=min(ans,(ll)(g[n][i]+val));
	}
	if(flag){
		printf("%lld\n",ans%mod);
		return;
	}
	
	for(int i=n-17;i>=1;i--){
		val=(val+(ll)(s[i]-'0')*mi)%mod; (mi*=2ll)%=mod;
		if(f[n][i]!=0){
			printf("%lld\n",(ll)(g[n][i]+val)%mod);
			return;
		}
	}

}
signed main(){
	cin>>s;
	n=s.size(); s=' '+s;
	Init();
	solve1();
	solve2();
	return 0;
}



18.CF1562E Rescue Niwen!

sol1(人類智慧)

\(O(n^2)\) DP 求出所有 \(LCP(i,j)\)
\(f[l,r]\) 表示以 \([l,r]\) 這個子串為末尾的 LIS , \(F[i]\) 表示 \(f[i,n]\)
因為注意到對於 以 \([l,r]\) 這個子串為末尾的 LIS , 我一定可以往後面接 \([l,r+1] [l,r+2], ... ,[l,n]\)
所以最後的答案一定是 \(\max ^n_{i=1} f[i,n]\) ,即 \(\max ^n_{i=1} F[i]\)

\(LCP(i,j)=len (j<i)\) :

  1. 如果 \(s[j+len]>s[i+len]\) ,則無法產生貢獻
  2. 如果 \(s[j+len]<s[i+len]\)\(F[i]=\max(F[i],F[j] + n - (i+len) +1)\)
    就是說對於以 \([j,n]\) 這個子串為末尾的 LIS 再接上 \([i,i+len] , [i,i+len+1] , ... ,[i,n]\)

時間複雜度 \(O(n^2)\)

sol2(常規做法)

還有一種解法是根據 \(LCP\) 排序 (即按字典序排序),然後離散化,給每一個字串賦一個代表他排名的值。
再按照題目要求排序,跑正常的 \(LIS\)

時間複雜度\(O(n^2 \log n^2)\),看運氣可過。

code(sol1)

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e5+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int T;
int n,lcp[5005][5005],f[5005];
string s;
signed main(){
	T=read();
	while(T--){
		n=read();
		cin>>s; s=' '+s;
		for(int i=1;i<=n;i++){
			f[i]=0;
			for(int j=1;j<=n;j++){
				lcp[i][j]=0;
			}
		}
		lcp[n][n]=1;
		for(int i=n;i>=1;i--){
			for(int j=n;j>=1;j--){
				if(i==n&&j==n) continue;
				if(s[i]==s[j]) lcp[i][j]=lcp[i+1][j+1]+1;
				else lcp[i][j]=0;
			}
		}
		int ans;
		ans=n;
		for(int i=1;i<=n;i++){
			f[i]=n-i+1;
			for(int j=1;j<i;j++){
				if(s[j+lcp[i][j]] < s[i+lcp[i][j]])
					f[i]=max(f[i],f[j]+n-(i+lcp[i][j])+1);
			}
			ans=max(ans,f[i]);
		}
		printf("%d\n",ans);
	}
	return 0;
}



19.CF1701E Text Editor

若沒有 home 操作,很明顯不會用 right , 此時答案為 \(n - LCP(S,T)\) ( LCP 為最長公共字首)。
如果有 home , 此時策略一定是這樣的:

  1. 從後往前刪,每一次花費 \(1\) 進行 leftBackspace
  2. \(1\)home
  3. 從前往後刪,每一次花費 \(1\)right 或 花費 \(2\) 先按 right 再按 Backspace
  4. 剩餘中間一段 \(S,T\) 是一樣的不用動

考慮從前往後 和 從後往前 兩次 DP , 以從前往後為例:
\(f[i][j]\) 表示從前往後操作 , 當前操作完游標在 \(i\) ,讓 \(S[1,i]\)\(T[1,j]\) 匹配的最小代價。
轉移:

  • 刪除 \(f[i][j]=min(f[i][j],f[i-1][j]+2)\)
  • 不刪 若 \(s[i]=t[j]\) , \(f[i][j]=min(f[i][j],f[i-1][j-1]+1)\)

從後往前類似,用 \(g[i][j]\) 表示 , 當前操作完游標在 \(j\) ,讓 \(S[i,n]\)\(T[j,m]\) 匹配的最小代價

但是注意到我們還會有一段,即 剩餘中間一段 \(S,T\) 是一樣的不用動, 而在我們上面的轉移中,這一段我們也會用移動建,但其實是不用的。
所以用 \(F[i][j]\) 表示從前往後讓 \(S[1,i]\)\(T[1,j]\) 匹配的最小操作次數

  • 刪除 \(F[i][j]=min(F[i][j],f[i-1][j]+2)\) , 因為要刪除的話,一定是要把游標移到 \(i\) 之前的,所以轉移用 \(f\)陣列
  • 不刪 若\(s[i]=t[j]\) , \(F[i][j]=min(F[i][j],F[i-1][j-1])\) 可以不用移動。

\(G[i][j]\) 的定義和轉移類似

最後的答案是:

\[ \min {f[i][j] + g[i+1][j+1] + (f[i][j]!=0)} \]

最後一個是 home 操作的花費

這題卡空間,用 \(short\) 儲存即可 (因為不能用滾動陣列)。

code

#include<bits/stdc++.h>
using namespace std;
const int N=5e3+5,inf=N;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int T,n,m;
short f[N][N],g[N][N],F[N][N],G[N][N];
char s[N],t[N];
int Min(int x,int y){  //之所以手寫是因為min不能比較 short 和 int 
	if(x>y) return y;
	return x;
}
signed main(){
	T=read();
	while(T--){
		n=read(),m=read();
		scanf("%s%s",s+1,t+1);
		for(int i=0;i<=n+1;i++){
			for(int j=0;j<=m+1;j++){
				F[i][j]=G[i][j]=f[i][j]=g[i][j]=inf;
			}
		}
		
		F[0][0]=f[0][0]=0;
		for(int i=1;i<=n;i++){
			for(int j=0;j<=m;j++){    //注意把 j=0 迴圈上 
				F[i][j]=f[i][j]=f[i-1][j]+2;
				if(s[i]==t[j])	f[i][j]=Min(f[i][j],f[i-1][j-1]+1),F[i][j]=Min(F[i][j],F[i-1][j-1]);
			}
		}
		
		G[n+1][m+1]=g[n+1][m+1]=0;
		for(int i=n;i>=1;i--){
			for(int j=m+1;j>=1;j--){   //注意把 j=m+1 迴圈上 
				G[i][j]=g[i][j]=g[i+1][j]+1;
				if(s[i]==t[j]) g[i][j]=Min(g[i][j],g[i+1][j+1]+1),G[i][j]=Min(G[i][j],G[i+1][j+1]);
			}
		}
		
		int ans=inf;
		for(int i=0;i<=n;i++){
			for(int j=0;j<=m;j++){
				ans=min(ans,F[i][j]+G[i+1][j+1]+(F[i][j]!=0));
			}
		}
		
		if(ans==inf) printf("-1\n");
		else printf("%d\n",ans);
	}
	return 0;
}

20.CF1954D Colored Balls

經典結論:一個集合的答案為 \(max(\lceil \frac {sum}{2} \rceil,maxn)\)
\(sum\) 為集合元素總個數,\(maxn\) 為出現次數最多的數的個數,除法取上整

揹包求方案數再乘上答案即可。

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=5e3+5,mod=998244353;
inline int read(){
	int w=1,s=0;
	char c=getchar();
	for(;c<'0'||c>'9';w*=(c=='-')?-1:1,c=getchar());
	for(;c>='0'&&c<='9';s=s*10+c-'0',c=getchar());
	return w*s;
}
int n;
int a[N],f[N],g[N];
int calc(int x){
	if(x&1) return x/2+1;
	return x/2;
}
signed main()
{
	n=read();
	for(int i=1;i<=n;i++) a[i]=read();
	sort(a+1,a+n+1);
	int sum=0,ans=0;
	f[0]=1;
	for(int i=1;i<=n;i++){
		sum+=a[i];
		for(int j=N-1;j>=a[i];j--){
			g[j]=f[j-a[i]];
			(f[j]+=f[j-a[i]])%=mod;
		}
		for(int j=a[i];j<=sum;j++)
			(ans+=g[j]*max(calc(j),a[i]))%=mod;
	}
	printf("%lld\n",ans);
	return 0;
}

21.染色

題目大意:給你一個序列,每個位置有一個顏色,求把這個序列分成若干段,每段顏色數的平方之和的最小值。

暴力DP \(O(n^2)\): \(f[i]\) 表示把前 \(i\) 個位置染成對應顏色的最小值,轉移時列舉 \(j\) , \(f[i]=min(f[i],f[j]+calc(j,i)^2)\) calc表示顏色數量

經典套路之---考慮DP值的範圍:
注意到最終答案一定不大於n,因為我完全可以每一次只染色長度為 \(1\) 的區間
所以顏色數我們只用列舉到 \(\sqrt n\) 即可

具體來講: 因為我們確定了顏色數之後,肯定一次染的長度越多越好,而顏色數我們只需要知道每個顏色的最後一個位置即可,所以我們只需要維護最後 \(\sqrt n\) 個顏色的最後一個位置,\(O(n \times \sqrt n)\)轉移

code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e4+5,inf=5e5+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,a[N],f[N];
unordered_map<int,int> pos;
set<int> s;
signed main(){
//	freopen("cyq.in","r",stdin);
//	freopen("cyq.out","w",stdout);
	while(scanf("%lld",&n)!=EOF){
		for(int i=1;i<=n;i++) a[i]=read(),f[i]=inf;
		int t=sqrt(n)+1+1;
		f[0]=0;  //注意不能寫f[1]=1 
		s.clear();
		s.insert(0);
		for(int i=1;i<=n;i++){
			if(!pos[a[i]]||(s.find(pos[a[i]])==s.end())){
				pos[a[i]]=i,s.insert(i);
				if(s.size()>t) s.erase(s.begin());
			}
			else{
				s.erase(pos[a[i]]);
				pos[a[i]]=i;
				s.insert(pos[a[i]]);
			}
			int cnt=s.size()-1;
			for(int x:s){
				if(cnt==0) break;
				f[i]=min(f[i],f[x]+cnt*cnt);   //染色染[x+1,i] 
				cnt--;
			}
		}
		printf("%lld\n",f[n]);
		for(int i=1;i<=n;i++) pos.erase(a[i]);
	}
	return 0;
}

22.二進位制翻轉

對於操作序列:
\((x_1,y_1),(x_2,y_2),...,(x_k,y_k)\)
如果 \(x_i=x_j\) , 那我們可以把他們消掉,對於 y 也同理
假設消掉後剩下 \(a\) 個互不相同 \(x\),和 \(b\) 個互不相同的 \(y\),那麼容易得到還剩 \(a\times m+b\times n-2\times a \times b\)\(1\),我們完全可以暴力列舉這個 \(a\)\(b\),我們只需要分開計算方案數即可。

\(f[i][j]\) 表示 構造長度為 \(i\) 的序列 \({x_1,x_2,...,x_i}\),按如上方法消掉之後剩餘 \(j\) 個的方案數,轉移時:

  1. \(i\)\(x\) 和前面的某個 \(x\) 抵消了,所以 \(f[i-1][j+1] \times (j+1) \to f[i][j]\)
  2. \(i\)\(x\) 和前面的任何一個 \(x\) 都不一樣,所以 \(f[i-1][j-1] \times (n-j+1) \to f[i][j]\)

\(g[i][j]\)同理

code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3005,mod=1e9+7;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,k,s; 
int f[N][N],g[N][N];
signed main(){
	n=read(),m=read(),k=read(),s=read();
	
	f[0][0]=1;
	for(int i=1;i<=k;i++){
		for(int j=0;j<=n;j++){
			f[i][j]=f[i-1][j+1]*(j+1)%mod;
			if(j>0) (f[i][j]+=f[i-1][j-1]*(n-j+1)%mod)%=mod;
		}
	}
	
	g[0][0]=1;
	for(int i=1;i<=k;i++){
		for(int j=0;j<=m;j++){
			g[i][j]=g[i-1][j+1]*(j+1)%mod;
			if(j>0) (g[i][j]+=g[i-1][j-1]*(m-j+1)%mod)%=mod;
		}
	}
	
	int ans=0;
	for(int a=0;a<=n;a++){
		for(int b=0;b<=m;b++){
			if(a*m+b*n-2ll*a*b!=s) continue;
			if(a>k||b>k||((k-a)%2ll!=0)||((k-b)%2ll!=0)) continue;
			(ans+=f[k][a]*g[k][b]%mod)%=mod;
		}
	}
	printf("%lld\n",ans);
	return 0;
}

23.不穩定的傳送門

\(f[i]\) 表示 \(i\)\(n\) 的最小期望花費,令 \(f[n]=n\)

假設 \(i\) 的所有出邊為 \((t_j,p_j,w_j) (1 \le j \le cnt_i)\),\(cnt_i\)\(i\) 的出邊數量。
我們按照一定順序安排嘗試順序後則

\[期望 =w_1 + p_1 \times f[t_1] + (1-p_1) \times (w_2 + p_2 \times f[t_2] +(1 - p_2) \times (...) ) \]

我們適當換一下元,令 \(c_j=w_j+p_j \times f[t_j]\) (不是題目描述的 \(c\)),則:

\[\begin{aligned} 期望 &=c_1+(1-p_1) \times (c_2+(1-p_2) \times (c_3+(1-p_3) \times (...) ) ) \\ &=c_1 + (1-p_1)\times c_2 + (1-p_1)\times (1-p_2) \times c_3 + ... \\ &= \sum_{j=1}^{cnt_i} (\prod_{k=1}^{j-1} 1-p_k) \times cj \\ \end{aligned} \]

要使它儘可能小,我們考慮鄰項交換:
對於相鄰兩項 j,j+1,
原來的期望花費 \(= (1-p_1) \times (1-p_2)\times ...\times (1-p_{j-1})\times c_j + (1-p_1)\times (1-p_2)\times ...\times (1-p_{j-1})\times (1-p_j)\times c_{j+1}\)
交換之後的花費 = \(= (1-p_1) \times (1-p_2)\times ...\times (1-p_{j-1})\times c_{j+1} + (1-p_1)\times (1-p_2)\times ...\times (1-p_{j-1})\times (1-p_{j+1})\times c_j\)
當前一項比後一項小時,消去相同的項得到:
\(c_j+(1-p_j)\times c_{j+1} < c_{j+1}+(1-p_{j+1})\times c_j\)
按照這個寫 cmp 即可

code

include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m;
struct P{
	int t;
	double p;
	int w;
	double c;
};
vector<P> G[N];
double f[N];
bool cmp(P x,P y){
	return 1.0*x.c+(1.0-x.p)*y.c < 1.0*y.c+(1.0-y.p)*x.c;
}
signed main(){
	n=read(),m=read();
	for(int i=1;i<n;i++){
		int w=read();
		G[i].push_back({i+1,1.0,w,0});
	}
	for(int i=1;i<=m;i++){
		int s=read(),t=read();
		double p;
		cin>>p;
		int w=read();
		G[s].push_back({t,p,w,0});
	}
	f[n]=0;
	for(int u=n-1;u>=1;u--){
		for(int i=0;i<G[u].size();i++)
			G[u][i].c=G[u][i].w*1.0+G[u][i].p*f[G[u][i].t];
		sort(G[u].begin(),G[u].end(),cmp);
		double g=1;
		for(P e:G[u]){
			f[u]+=e.c*g;
			g*=(1.0-e.p);
		}	
	}
	
	printf("%.2lf",f[1]);
	return 0;


24.CF1444D Rectangular Polyline

這題其實沒有什麼動態規劃,主要是構造,
考慮到這個是動態規劃題單,並且題解有點複雜,所以具體見Booksnow 的題解
首先排除我懶

只需要注意下涉及到的 bitset最佳化01揹包

code

#include<bits/stdc++.h>
using namespace std;
const int N=1e3+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int T;
int n,m,l[N],p[N],sumx,sumy; 
vector<int> x[2],y[2];
int dx[N],dy[N],cnt;
bitset<N*N> f[N];
signed main(){
	T=read();
	f[0][0]=1;    //不需要重複賦值 
	while(T--){
		sumx=sumy=0;
		x[0].clear(),x[1].clear();
		y[0].clear(),y[1].clear();
		
		n=read();
		for(int i=1;i<=n;i++) l[i]=read(),sumx+=l[i];
		m=read();
		for(int i=1;i<=m;i++) p[i]=read(),sumy+=p[i];
		if(n!=m||(sumx&1)||(sumy&1)){
			puts("No");
			continue;
		}
		
		for(int i=1;i<=n;i++) f[i]=f[i-1]|(f[i-1]<<l[i]);
		if(!f[n][sumx/2]){
			puts("No");
			continue;
		}
		int lst=sumx/2;
		for(int i=n;i>=1;i--)
			if(lst>=l[i]&&f[i-1][lst-l[i]]) lst-=l[i],x[0].push_back(l[i]);
			else x[1].push_back(l[i]);
			
		for(int i=1;i<=m;i++) f[i]=f[i-1]|(f[i-1]<<p[i]);
		if(!f[m][sumy/2]){
			puts("No");
			continue;
		}
		lst=sumy/2;
		for(int i=m;i>=1;i--)
			if(lst>=p[i]&&f[i-1][lst-p[i]]) lst-=p[i],y[0].push_back(p[i]);
			else y[1].push_back(p[i]);
			
		if(x[0].size()>y[0].size()) swap(x[0],x[1]);
		if(x[0].size()>y[0].size()) swap(y[0],y[1]);
		
		puts("Yes");
		cnt=0;
		for(int v:x[0]) dx[++cnt]=v;
		for(int v:x[1]) dx[++cnt]=v;
		cnt=0;
		for(int v:y[0]) dy[++cnt]=v;
		for(int v:y[1]) dy[++cnt]=v;
	
		sort(dx+1,dx+x[0].size()+1,greater<int>());
		sort(dx+x[0].size()+1,dx+y[0].size()+1,greater<int>());
		sort(dx+y[0].size()+1,dx+n+1,greater<int>());
		
		sort(dy+1,dy+x[0].size()+1);
		sort(dy+x[0].size()+1,dy+y[0].size()+1);
		sort(dy+y[0].size()+1,dy+m+1);
		int X=0,Y=0;
		for(int i=1;i<=x[0].size();i++){
			X+=dx[i];printf("%d %d\n",X,Y);
			Y+=dy[i];printf("%d %d\n",X,Y);
		}
		for(int i=x[0].size()+1;i<=y[0].size();i++){
			X-=dx[i];printf("%d %d\n",X,Y);
			Y+=dy[i];printf("%d %d\n",X,Y);
		}
		for(int i=y[0].size()+1;i<=n;i++){
			X-=dx[i];printf("%d %d\n",X,Y);
			Y-=dy[i];printf("%d %d\n",X,Y);
		}		
	}
	return 0;
}

25.CF1178F1 Short Colorful Strip

這是 F題的弱化版,保證了最終序列是個排列。

很明顯兩次染色操作要麼包含要麼相離,絕對不可能相交。

考慮區間DP: \(f[i][j]\) 表示染完區間 \([i,j]\) 的方案數
顯然我們先染的一定是最小的那個顏色,假設那個顏色的位置為 \(k\)
列舉第一次染色區間 \([x,y]\),顯然 \([x,y]\) 包含 \(k\)
\(l \le x \le k \le y \le r\)
因為後面就不能再染 \(k\) 這個位置了,且染色操作絕對不可能相交,所以區間: \([l,x-1],[x,k-1],[k+1,y],[y+1,r]\) 是獨立的,把他們的方案數乘起來即可。
注意這裡每個區間的先後順序是唯一的(按照最小顏色排),所以不用乘 \(4!\)
即:

\[ f[l,r]=\sum_{x=l}^{k}\sum_{y=k}^{r}(f[l,x-1] \times f[x,k-1]) \times (f[k+1,y] \times f[y+1,r]) \]

注意到這樣是 \(O(n^4)\) ,所以\(x,y\) 拆開列舉即可:

\[ f[l,r]=(\sum_{x=l}^{k}f[l,x-1] \times f[x,k-1]) \times (\sum_{y=k}^{r}f[k+1,y] \times f[y+1,r]) \]

code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5,mod=998244353;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,a[505],f[505][505];
signed main(){
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	n=read(),m=read();
	for(int i=1;i<=m;i++) a[i]=read();
	for(int l=0;l<=m+1;l++){
		for(int r=0;r<=m+1;r++){
			f[l][r]=1;
		}
	}
	for(int len=1;len<=m;len++){
		for(int l=1;l+len-1<=m;l++){
			int r=l+len-1;
			if(len>1){
				int sum1=0,sum2=0,ming=INT_MAX,pos;
				for(int i=l;i<=r;i++)
					if(a[i]<ming) ming=a[i],pos=i;
				for(int x=l;x<=pos;x++) (sum1+=f[l][x-1]*f[x][pos-1])%=mod;
				for(int y=pos;y<=r;y++) (sum2+=f[pos+1][y]*f[y+1][r])%=mod;
				f[l][r]=sum1*sum2%mod;
			}
		}
	}
	printf("%lld\n",f[1][m]);
	return 0;
}

26.CF1178F2 Long Colorful Strip

這題和上一題唯一的區別就是 : 這題紙帶很長,每一種顏色不一定只有一種。
為了套用上一題的區間DP做法,考慮怎麼縮小 \(m\)
注意到,從一開始的一個顏色段,每一次操作最多增加 \(2\) 個顏色段,即,如果最終狀態顏色段的數目\(>2\times n+1\),那肯定無解。
並且對於一個最終狀態的顏色段,他們肯定是一起被染色的,否則後面就無法把他們一起染成目標顏色,於是可以把所有顏色段看成一個點,這樣最多有 \(2 \times n+1\) 個點,由於 \(O(n^3)\) 肯定跑不滿,所以可以借鑑上一題的做法。

因為每一種顏色不一定只有一種,即每一個區間 \([l,r]\) 不一定只有一個最小的顏色,所以我們列舉的 \([x,y]\) 要包含所有的 \(k_1,k_2,k_3,k_4,...k_{cnt}\)

轉移的式子為:

\[\begin{aligned} f[l,r] &=\sum_{x=l}^{k_1}\sum_{y=k_{cnt}}^{r}f[l,x-1] \times f[x,k_1-1] \times f[k_1+1,k_2-1] \times ... \times f[k_{cnt-1}+1,k_{cnt}-1] \times f[k_{cnt}+1,y] \times f[y+1,r]\\ &=(\sum_{x=l}^{k_1}f[l,x-1] \times f[x,k1-1]) \times (\sum_{y=k_{cnt}}^{r}f[kcnt+1,y] \times f[y+1,r]) \times (f[k1+1,k2-1] \times ... \times f[k[cnt-1]+1,kcnt-1]) \end{aligned} \]

實現時還有一個細節要注意的是,比如樣例中的: 2 1 2
雖然這一看就是 \(0\) ,但是程式會輸出 \(4\)
這是因為當我們根據 \(1\) 把它分成 \(2\)\(2\) 兩半後,這兩半並不是獨立的,要把它們染成 \(2\),必然要經過 \(1\)
特判也很好處理:如果 \([l,r]\) 這段區間裡的顏色中有顏色並沒有全部出現則把它的DP值設為 \(0\)

code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5,mod=998244353;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m;
int a[N],b[N],f[1005][1005],pre[1005][505];
signed main(){
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	n=read(),m=read();
	for(int i=1;i<=m;i++){
		b[i]=read();
	}
	int cnt=0;
	for(int i=1;i<=m;i++){
		int j;
		for(j=i;b[j]==b[i];j++) ;
		a[++cnt]=b[i],i=j-1;
	}
	m=cnt;
	if(m>2*n+1){
		printf("%d\n",0);
		return 0;	
	} 
	for(int i=1;i<=m;i++){
		pre[i][a[i]]=1;
	}
	for(int i=1;i<=m;i++){
		for(int j=1;j<=n;j++){
			pre[i][j]+=pre[i-1][j];
		}
	}
	for(int l=0;l<=m+1;l++){
		for(int r=0;r<=m+1;r++){
			f[l][r]=1;
		}
	}
	for(int len=1;len<=m;len++){
		for(int l=1;l+len-1<=m;l++){
			int r=l+len-1;
			for(int i=l;i<=r;i++){
				int cnt1=pre[r][a[i]]-pre[l-1][a[i]],cnt2=pre[m][a[i]];
				if(cnt1<cnt2){
					f[l][r]=0;
					break;
				}
			}
			if(len>1){
				int sum1=0,sum2=0,tmp=1,ming=INT_MAX;
				for(int i=l;i<=r;i++)
					if(a[i]<ming) ming=a[i];
				int st=-1,ed=-1,lst=-1;
				for(int i=l;i<=r;i++){
					if(a[i]==ming){
						if(lst!=-1)
							(tmp*=f[lst+1][i-1])%=mod;
						if(st==-1) st=i;
						lst=ed=i;
					}
				}
				for(int x=l;x<=st;x++) (sum1+=f[l][x-1]*f[x][st-1])%=mod;
				for(int y=ed;y<=r;y++) (sum2+=f[ed+1][y]*f[y+1][r])%=mod;
				f[l][r]*=sum1*sum2%mod*tmp%mod;
			}
		}
	}
	printf("%lld\n",f[1][m]);
	return 0;
}

27.[CEOI2016] kangaroo

題目要求相當於是說只能來回橫跳。

相當於要構造一個排列,這個排列滿足:
對於每個連續的三個數,中間那個數是最大的或最小的,即整個序列呈波浪型

關於這種序列滿足一定形狀的題,套路就是考慮從小到大插入每個位置
\(f[i][j]\) 表示插入到 \(i\) , 一共有 \(j\) 個連續段的情況,接下來對於 \(i+1\) ,如果 \(i+1 \ne s或t\),有三種情況:

  1. 自成一段,即插在空隙裡,一共有 \(j+1\) 個空隙,但如果此時已經插入了 \(s\)\(t\) , 頭或尾不能插
  2. 把兩段接在一起,此時 \(i+1\) 一定大於其左右兩個數(因為是從小到大),即他是波峰,一定滿足題意,一共有 \(j-1\) 個選擇
  3. 加在一段的開頭或結尾 , 假設接在開頭,那麼此時 i+1 的右邊比它要小,但左邊由於還沒放,到時候肯定比它要大,這樣就會出現單調遞減的三個數,就不符合題意了,所以不會出現這種情況。(當然如果 \(i+1=s或t\)時是可以的)

程式碼裡用的是填表法,在 \(i=s或t\) 時要特判,此時只能放在開頭/結尾。

code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e3+5,mod=1e9+7;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,s,t,f[N][N]; 
signed main(){
	n=read(),s=read(),t=read();
	f[0][0]=1;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=i;j++){
			if(i==s||i==t) f[i][j]=(f[i-1][j-1]+f[i-1][j])%mod;
			else f[i][j]=((j-(i>s)-(i>t))*f[i-1][j-1]%mod+j*f[i-1][j+1]%mod)%mod;
		}
	}
	printf("%lld\n",f[n][1]);
	return 0;
}

28.CF1312D Count the Arrays

套路和上題類似。

先假設我們是用給定的 \(n-1\) 個數來構造這個序列。
我們考慮從大到小考慮這 \(n-1\) 個數。

\(f[i][0/1]\) 表示構造長度為 \(i\) 的滿足條件的序列, 並且 沒有/有 出現兩個相同的數,那對於

  • \(f[i][0]\) :我可以把當前考慮的這個數放在序列的開始,也可以放在結尾 \(f[i][0]\gets 2 \times f[i-1][0]\)
  • \(f[i][1]\) :這個數如果不是相同的那個數則 \(f[i][1]\gets2* \times[i-1][1]\),否則我就開頭放一個,結尾放一個 \(f[i][1] \gets f[i-2][0]\)

我們只需要隨便從 \(m\) 個數裡選出 \(n-1\) 個數就好了,答案為 \(f[n][1] \times C_m^{n-1})\)

幾個細節:

  1. \(i=1\)時,放在開頭和結尾是一樣的,不用\(\times2\)
  2. 只有 \(i \ge 3\)時才能進行 \(f[i][1] \gets f[i-2][0]\) 的轉移,不然不滿足嚴格單調

code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+5,mod=998244353;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,f[N][2];
int inv[N],fact[N],q[N];
int C(int n,int m){
	if(n<m) return 0;
	return fact[n]*q[m]%mod*q[n-m]%mod; 
}
signed main(){
	n=read(),m=read();
	fact[0]=1;
	for(int i=1;i<N;i++) fact[i]=fact[i-1]*i%mod;
	inv[1]=1;
	for(int i=2;i<N;i++) inv[i]=(mod-mod/i)*inv[mod%i]%mod;
	q[0]=1;
	for(int i=1;i<N;i++) q[i]=q[i-1]*inv[i]%mod;

	f[0][0]=1;
	for(int i=1;i<=n;i++){
		if(i==1){
			(f[i][0]=f[i-1][0])%=mod;
			(f[i][1]=f[i-1][1])%=mod;
		}
		else{
			(f[i][0]=2*f[i-1][0])%=mod;
			(f[i][1]=2*f[i-1][1])%=mod;
		}
		if(i>=3) (f[i][1]+=f[i-2][0])%=mod;
	}
	printf("%lld\n",f[n][1]*C(m,n-1)%mod);
	return 0;
}

29.CF1312E Array Shrinking

考慮區間DP,一段區間 \([l,r]\) 要麼直接合成一個數,否則一定可以找到一個分界點 \(i\),使 \(f[l][r]=f[l][i]+f[i+1][r]\),分別進行DP即可。

code

#include<bits/stdc++.h>
using namespace std;
const int N=500+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,a[N];
int f[N][N];  //f[l][r]表示[l,r]剩餘的最小長度 
int g[N][N];  //g[l][r]表示區間[l,r]縮成一個數時的值,如果不能就=0 
signed main(){
	n=read();
	for(int i=1;i<=n;i++) a[i]=read();
	for(int len=1;len<=n;len++){   
		for(int l=1;l+len-1<=n;l++){
			int r=l+len-1;
			if(len==1) g[l][r]=a[l];
			else if(len==2){
				if(a[l]==a[r]) g[l][r]=a[l]+1; 
			}
			else{
				for(int i=l;i<=r;i++){
					if(g[l][i]==g[i+1][r]&&g[l][i]!=0){
						g[l][r]=g[l][i]+1;
						break;
					}
				}				
			}
		}
	}
	for(int len=1;len<=n;len++){
		for(int l=1;l+len-1<=n;l++){
			int r=l+len-1;
			f[l][r]=r-l+1;
			if(g[l][r]) f[l][r]=1;
			else{
				for(int i=l;i<=r;i++){
					f[l][r]=min(f[l][r],f[l][i]+f[i+1][r]);
				}
			}
		}
	}
	printf("%d\n",f[1][n]);
	return 0;
}

30.CF1312G Autocompletion

首先按照題目的意思可以建出一個Trie(這個建的方式有點奇怪,可以看程式碼)

假設 \(f[i]\) 表示列印出 \(i\) 號節點對應的字串所需要的最小花費,\(id[i]\) 表示 \(i\) 號節點對應的字串在 \(S\) 中的字典序。

做一個樹形DP: \(f[i]=min(f[fa]+1,f[j]+id[i]-id[j])\), 其中 \(j\)\(i\) 的祖先,即 \(j\) 表示的字串是 \(i\) 的字首。
考慮最佳化後面的轉移,用一個棧來儲存 \(f[i]-id[i]\)
在遍歷到 \(i\) 的時候如果棧頂值比我大,那就入棧,回溯如果棧裡面有就出棧
,這樣轉移時直接取出棧頂即可,並且保證了棧裡面的一定都是 i 的祖先

\(id\) 陣列也不用真的建出來,只需要用變數維護即可,只不過那些不是 S 中的節點是不能算在裡面的。

code

#include<bits/stdc++.h>
#define PII pair<int,int>
#define fi first
#define se second
using namespace std;
const int N=1e6+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,id,a[N];
int ch[N][30],f[N];
bool stater[N];
stack<PII> st;
void dfs(int u){ //遍歷到 u 的時候 f[u] 已經算出來了
	if(!st.size()||(st.top().se>f[u]-id)) st.push({u,f[u]-id});
	id+=stater[u];   //這裡 id 其實表示的是比它字典序小的在 S 中的個數 
	for(int i=0;i<=25;i++){
		if(!ch[u][i]) continue;
		int v=ch[u][i];
		f[v]=f[u]+1;
		if(st.size()&&stater[v]) f[v]=min(f[v],st.top().se+id+1);  
		/*
			首先自動補全的結果要是S中的字串,所以要滿足 stater[v]=true
			其次,id[v]-id[u]在這裡表示的是從u開始到v(包括u,不包括v)中字典序比 v 小的個數,但實際上是要算上 v 的,所以要 +1 
		*/ 
		dfs(v); 
	}
	if(st.size()&&st.top().fi==u) st.pop();
}
signed main(){
	n=read();
	for(int i=1;i<=n;i++){
		int x=read();
		char c;
		cin>>c;
		ch[x][c-'a']=i;
	}
	int m=read();
	for(int i=1;i<=m;i++){
		a[i]=read();
		stater[a[i]]=true;
	}
	
	dfs(0);
	
	for(int i=1;i<=m;i++) printf("%d ",f[a[i]]);
	puts("");
	return 0;
}

31.CF1580D Subsequence

不會笛卡爾樹,但是看到題解區的妙妙解法......

題目的式子非常大便,我們考慮把它翻譯成人話:
一個子序列的價值為: \(sum*m - 每兩個數及他們之間的所有數的\min\)

\(f[l][r][k]\) 表示在 \([l,r]\) 中選出 \(k\) 個數的最大代價(為了方便我們前面那一項不 \(\times k\),而是還是\(\times m\))。
假設 \([l,r]\) 的最小值位置是 \(pos\),如果最後選出的數不跨越 \(pos\),可以遞迴分治。
現在考慮跨越 \(pos\) 的情況,我們有兩個轉移:

  1. \(f[l][pos-1][i]+f[pos+1][r][j] - 2 \times i \times j \times a[pos] \to f[l][r][i+j]\)
  2. \(f[l][pos-1][i]+f[pos+1][r][j] + m\times a[pos] - [2\times (i+1) \times (j+1)-1] \times a[pos] \to f[l][r][i+j+1]\)

我們來解釋一下:

  1. 不選擇 \(a[pos]\),那麼左端點在 \(a[pos]\) 左邊,右端點在 \(a[pos]\) 右邊的貢獻就是 \(-2\times i\times j\times a[pos]\) (\(\times 2\)是因為每一對會貢獻兩次,因為題目中的 \(j\) 是從 \(1\) 開始,而不是 \(i\))
  2. 選擇 \(a[pos]\),那麼左端點 \(pos\) 或在 \(a[pos]\) 左邊,右端點 \(=pos\) 或在 \(a[pos]\) 右邊的貢獻是 \(-[2\times (i+1)\times (j+1)-1]\times a[pos]\)\(-1\) 是因為當計算題目中的 \(f\) 時左右端點都在 \(pos\) 時只能貢獻一次

遞迴分治求解,時間複雜度可以近似認為是:
\(T(n)=2 \times T(\frac{n}{2})+O(n^2)\)
根據主定理他等於 \(O(n^2)\)

code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,a[N]; 
vector<int> work(int l,int r){
	vector<int> f(r-l+2,LLONG_MIN);
	f[0]=0;
	if(l>r) return f;
	int pos=l;
	for(int i=l+1;i<=r;i++) if(a[pos]>a[i]) pos=i;
	vector<int> fl=work(l,pos-1),fr=work(pos+1,r);
	for(int i=0;i<fl.size();i++){
		for(int j=0;j<fr.size();j++){
			f[i+j]=max(f[i+j],fl[i]+fr[j]-2*i*j*a[pos]);
			f[i+j+1]=max(f[i+j+1],fl[i]+fr[j]+m*a[pos]-(2*(i+1)*(j+1)-1)*a[pos]);
		}
	} 
	return f;
}
signed main(){
	n=read(),m=read();
	for(int i=1;i<=n;i++) a[i]=read();
	printf("%lld\n",work(1,n)[m]);
	return 0;
}

32.P1220 關路燈

經典套路之——費用提前計算

如果設 \(f[l][r][0/1]\) 表示關掉區間 \([l,r]\) 內的路燈後,且最後停在 \(l/r\),區間 \([l,r]\) 的路燈的最小花費。
不難想到是從 \(f[l+1][r]\)\(f[l][r-1]\) 轉移過來。
但是轉移時我們無法知道從開始到現在經過的時間,也就無法知道新加進來的路燈到底消耗了多少。

\(f[l][r][0/1]\) 表示關掉區間 \([l,r]\) 內的路燈後,且最後停在 \(l/r\),所有路燈的最小花費。
以從 \(f[l+1][r][0]\) 轉移到 \(f[l][r][0]\) 為例,在轉移時,我們不用再考慮在關 \([l+1,r]\) 內的路燈時的其他路燈的消耗。
只需要加上從 \(l+1\) 走到 \(l\) 這段時間裡,\([1,l]\)\([r+1,n]\) 路燈的功率消耗。
字首和最佳化一下即可,轉移顯然。

code

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,c,a[55],w[55],f[55][55][2];
int pre[55];
int calc(int l,int r){
	return pre[r]-pre[l-1];
}
signed main(){
	n=read(),c=read();
	for(int i=1;i<=n;i++){
		a[i]=read(),w[i]=read();
		pre[i]=pre[i-1]+w[i];
	}
	memset(f,0x3f,sizeof f);
	for(int len=1;len<=n;len++){
		for(int l=1;l+len-1<=n;l++){
			int r=l+len-1;
			if(c<l||c>r) continue;
			if(len==1) f[l][r][0]=f[l][r][1]=0;
			else{
				f[l][r][0]=min( f[l+1][r][0] + (a[l+1]-a[l])*(pre[n]-calc(l+1,r)) , f[l+1][r][1] + (a[r]-a[l])*(pre[n]-calc(l+1,r)) );
				f[l][r][1]=min( f[l][r-1][1] + (a[r]-a[r-1])*(pre[n]-calc(l,r-1)) , f[l][r-1][0] + (a[r]-a[l])*(pre[n]-calc(l,r-1)) );
			}
		}
	}
	printf("%d\n",min(f[1][n][0],f[1][n][1]));
	return 0;
}

33.搶鮮草

和上題幾乎一模一樣。

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e3+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,x,a[N];
int f[N][N][2];
/*
	考慮貢獻提前計算 
	f[l][r][0/1]: 表示kona採集完所有的 [l,r] 中的草,最後在 i/j 時,所有草的最小損失青草量
	(注意"所有") 
*/
signed main(){
	n=read(),x=read();
	for(int i=1;i<=n;i++) a[i]=read();
	sort(a+1,a+n+1);
	
	memset(f,0x3f,sizeof f); 
	for(int len=1;len<=n;len++){
		for(int l=1;l+len-1<=n;l++){
			int r=l+len-1;
			if(l==r){
				f[l][r][0]=f[l][r][1]=abs(a[l]-x)*n;	
			}
			else{
//				f[l][r][0]=min(f[l+1][r][0] + (a[l+1]-a[l]) * (n-(r-(l+1)+1)),f[l+1][r][1] + (a[r]-a[l]) * (n-(r-(l+1)+1)) );   //最原始的式子方便理解 
				f[l][r][0]=min(f[l+1][r][0] + (a[l+1]-a[l]) * (n-r+l),f[l+1][r][1] + (a[r]-a[l]) * (n-r+l));
				f[l][r][1]=min(f[l][r-1][1] + (a[r]-a[r-1]) * (n-r+l),f[l][r-1][0] + (a[r]-a[l]) * (n-r+l));
			}
		}
	}
	printf("%lld\n",min(f[1][n][0],f[1][n][1]));
	return 0;
}


34.P4161 [SCOI2009] 遊戲

如果把它根據對應關係連邊會得到若干環,假設第 \(i\) 個環的環長為 \(len[i]\)。那麼這個環裡的數要恢復原樣就要 \(len[i]\) 步。進一步的,容易得出整個序列要恢復原樣需要 \(lcm(len[1],len[2],...)\)
問題轉化為:給你一個 \(n\) , 構造若干個數,使得他們的和為 \(n\), 求他們的 \(lcm\) 的可能的情況數。

怎麼構造出儘可能多的 \(lcm\)?
如果每一次這若干個數都互質,那他們的 \(lcm = 他們的乘積\),從而每一次都不一樣。
具體的,我們只需要讓每個數都形如 \(pi^{ki}\) (\(pi\)是質數,\(ki\)是非負整數) , 那根據算數基本定理,只要方案不同,他們的乘積(\(lcm\))就不同。

小小的證明:如果有一個構造方案,裡面的數不互質,我們完全可以把它們轉換成形如上述的數列(每個質因子只保留對應的一個數),並且 \(lcm\) 不變,而且他們的總和會變小,我們只需要再往裡面塞 \(1\) ,就可以了。
即任意一個情況都可以轉化成上述構造方法。

\(f[i][j]\) 表示用前 \(i\) 個質數,湊出和為 \(j\) 的方案數,\(f[i][j]=f[i-1][j-pi^k]\),因為可以往裡面塞好多個 \(1\) , 所以我們欽定 \(k>0\);因為 \(i\) 可以不選,所以 \(f[i][j]+=f[i-1][j]\)
初始化 \(f[0][0]=1\)
最後的答案是 \(\sum_{j=0}^{n} f[max_i][j]\) (\(max_i\)是最大的不超過 \(n\) 的質數) 。

code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e3+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n;
bool stater[N];
int cnt,pri[N];
void Eular(int n){
	stater[1]=1;
	for(int i=2;i<=n;i++){
		if(!stater[i]) pri[++cnt]=i;
		for(int j=1;j<=cnt&&i*pri[j]<=n;j++){
			stater[i*pri[j]]=true;
			if(i%pri[j]==0) break;
		}
	}
}
int f[N][N];
signed main(){
	n=read();
	Eular(n); 
	
	f[0][0]=1;
	for(int i=1;i<=cnt;i++){
		for(int j=0;j<=n;j++){
			f[i][j]=f[i-1][j];
			for(int k=pri[i];k<=j;k*=pri[i]) f[i][j]+=f[i-1][j-k];
		}
	}
	int ans=0;
	for(int j=0;j<=n;j++) ans+=f[cnt][j];
	printf("%lld\n",ans);
	return 0;
}

35.P6280 [USACO20OPEN] Exercise G

這題和上一題基本上是一樣的。

  1. 一樣的圖論連邊得出每個排列需要的步數為\(lcm(len[i])\)
  2. 一樣的分析過程:我們只需要讓每個數都形如 \(pi^{ki}\) (\(pi\)是質數,\(ki\)是非負整數)。
  3. 基本一樣的DP狀態:
    \(f[i][j]\) 表示用前 \(i\) 個質數,湊出和為 \(j\) 的所有 \(lcm\) 的和。
  4. 基本一樣的轉移:\(f[i][j]=f[i-1][j-i^k]*(i^k)\) (因為互素,所以直接乘以 \(i^k\) 就是新的 \(lcm\))。
  5. 唯一不一樣的:\(N=1e4\),所以滾動陣列。

code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,mod;
bool stater[N];
int cnt,pri[N];
void Eular(int n){
	stater[1]=1;
	for(int i=2;i<=n;i++){
		if(!stater[i]) pri[++cnt]=i;
		for(int j=1;j<=cnt&&i*pri[j]<=n;j++){
			stater[i*pri[j]]=true;
			if(i%pri[j]==0) break;
		}
	}
}
int f[N];
signed main(){
	n=read(),mod=read();
	Eular(n); 
	
	f[0]=1;
	for(int i=1;i<=cnt;i++){
		for(int j=n;j>=0;j--){
			for(int k=pri[i];k<=j;k*=pri[i]) (f[j]+=f[j-k]*k%mod)%=mod;
		}
	}
	int ans=0;
	for(int j=0;j<=n;j++) (ans+=f[j])%=mod;
	printf("%lld\n",ans);
	return 0;
}

36.P6570 [NOI Online #3 提高組] 優秀子序列

一個數的二進位制表示可以看成是一個集合。
\(dp[i]\) 表示選出若干個互不相交的集合使他們的併為 \(i\) 的方案數,則答案是 \(\sum dp[i] \times \phi(i+1)\)。因為互不相交,所以他們的和也就等於他們的並等於 \(i\)

我們先認為原序列沒有 \(0\)
\(dp[i]=∑dp[i \oplus j]*cnt[j]\) (\(j\)\(i\) 的子集,\(cnt[j]\) 表示原序列 \(j\) 出現的次數)。為了避免重複計算,當 \(j < i \oplus j\) (補集) 時就退出。

對於有 \(0\) 的情況,我們往裡面可以塞任意多個 \(0\) , 所以最後 \(dp[i]=dp[i] \times 2^{cnt[0]}\)

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int M=1e6+5,N=(1<<18)+5,mod=1e9+7;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,a[M];
int pri[N],tot;
bool stater[N];
int mn[N],mx[N],k[N],phi[N];
/*
	mn[i]:i的最小質因子
	mx[i]:i中所含 mn[i] 的最大冪
	k[i]:i中所含 mn[i] 的個數,mx[i]=mn[i]^k[i] 
	phi[i]:i的尤拉函式 
*/
void Eular()
{
	stater[1]=1;
	phi[1]=1;
	for(int i=2;i<N;i++)
	{
		if(!stater[i]) pri[++tot]=i,mn[i]=i,mx[i]=i,k[i]=1,phi[i]=i-1;
		for(int j=1;j<=tot&&i*pri[j]<N;j++)
		{
			int x=i*pri[j];
			stater[x]=true;
			mn[x]=pri[j];
			if(i%pri[j]==0)
			{
				mx[x]=mx[i]*pri[j];
				k[x]=k[i]+1;
				if(x!=mx[x]) phi[x]=phi[x/mx[x]]*phi[mx[x]];
				else phi[x]=x/mn[x] * (mn[x]-1);    
				/*
					phi(p^k) = p^k - p^(k-1)
							 = p^(k-1) * (p-1)
				*/
				break;
			}
			else
			{
				mx[x]=pri[j];
				k[x]=1;
				phi[x]=phi[i]*phi[pri[j]];
			}
		}
	}
}
int quick_power(int a,int b){
	int ans=1;
	while(b){
		if(b&1) (ans*=a)%=mod;
		(a*=a)%=mod,b>>=1;
	}
	return ans;
}
int Max(int m,int x){
	for(int i=x;i;i-=(i&-i)){
		m=max(m,(int)log2(i&-i));
	}
	return m;
}
int cnt[M],f[M];
signed main(){
	Eular();
	n=read();
	int m=0;
	for(int i=1;i<=n;i++) a[i]=read(),m=Max(m,a[i]),cnt[a[i]]++;
	m++;
	
	f[0]=1;
	for(int i=1;i<(1<<m);i++){
		for(int s=i;s>=(i^s);s=(s-1)&i){
			(f[i]+=f[i^s]*cnt[s]%mod)%=mod;
		}
	} 
	
	int ans=0;
	for(int i=0;i<(1<<m);i++) (f[i]*=quick_power(2,cnt[0]))%=mod;
	for(int i=0;i<(1<<m);i++) (ans+=f[i]*phi[i+1]%mod)%=mod;
	printf("%lld\n",ans);
	return 0;
}

37.CF1280D Miss Punyverse

考慮樹形DP。
把每個點的點權 \(a[i]\) 設為 \(w[i]-b[i]\)
這樣只要看 \(a\) 之和是否 \(>0\)

\(f[u][i]\) 表示 \(u\) 這棵子樹,分成 \(i\) 個連通塊,不算包含 \(u\) 的那一塊,最大的 \(>0\) 的連通塊數,這樣不是很轉移的動,因為在合併時我們需要知道包含 \(u\) 的那一塊的權值之和。
考慮貪心,我們會發現有兩種比較理想的情況:

  1. 不算包含 \(u\) 的那一塊,其餘塊中滿足條件的塊記為 \(cnt\),\(cnt\) 儘可能大。
  2. 包含 \(u\) 的那一塊,目前的權值記為 \(val\),\(val\) 儘可能大。

事實上,肯定是按照第一種情況貪心更優,因為第二種情況裡,\(u\) 的那一塊權值再大也不過帶來 \(1\) 的貢獻,無法彌補第一種情況帶來的貢獻差。

用pair來儲存兩種情況,\(f[u][i].fi=cnt, f[u][i].se=val\)
我們只需要首先讓 \(cnt\) 儘可能大,其次才是讓 \(val\) 儘可能大。
邊界顯然是 \(f[u][1]={0,a[u]}\)
每次新加入一個子節點 \(v\), 我們有轉移:

  1. 包含 v 的那一塊單獨成一個連通塊: \(f[u][i+j].fi=f[v][j].fi+f[u][i].fi+(f[v][j].se > 0),f[u][i+j].se=f[u][i].se\)
    轉移時要優先按照第一維取max(pair自己內部就是這樣取max的,不用特殊處理) 。
  2. 包含 \(v\) 的那一塊合進包含 \(u\) 的那一塊: \(f[u][i+j-1].fi=f[v][j].fi + f[u][i].fi ,f[u][i+j-1].se=f[v][j].se + f[u][i].se\)

為了防止轉移時用的 f 陣列被更新過了,可以開一個輔助陣列暫存,也可以倒序列舉。

這個問題其實就是樹形揹包,但是由於這是動態規劃題單系列第一道樹形揹包題,所以簡要講一下:
對於一個狀態 \(f[u][i]\) 相當於有一個容積為 \(i\) 的揹包,每一個子節點 \(v\) 對應著一組物品,第 \(j\) 個物品大小為 \(j\),價值為 \(f[v][j]\),每組物品只能選一個(因為只能選一個轉移),使得在不超過揹包總體積的情況下(當然這道題因為轉移有點特殊其實不一定揹包體積是 \(i\)),價值的和(這道題裡不是簡單相加)最大,即分組揹包問題
對於複雜度的分析雖然看起來有點像是 \(O(n^3)\),
但注意到一次轉移我們其實可以認為是列舉了任意兩個子樹的大小,也可以認為是分別列舉了兩棵子樹內的點,一個點對最多隻會在 \(lca\) 處產生一個複雜度的貢獻,一共有 \(n^2\) 個點對,所以複雜度為 \(O(n^2)\)

code

#include<bits/stdc++.h>
typedef long long ll; 
#define PII pair<int,ll>
#define fi first
#define se second 
using namespace std;
const int N=3e3+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int T;
int n,m;
ll a[N];
int tot,head[N],to[N<<1],Next[N<<1];
void add(int u,int v){
	to[++tot]=v,Next[tot]=head[u],head[u]=tot;
} 
int Size[N];
PII f[N][N],tmp[N];
void dfs(int u,int fa){
	Size[u]=1;
	for(int i=0;i<=n;i++) f[u][i]={INT_MIN,INT_MIN};
	f[u][1]={0,a[u]};
	for(int i=head[u];i;i=Next[i]){
		int v=to[i];
		if(v==fa) continue;
		dfs(v,u);
		for(int j=1;j<=Size[u]+Size[v];j++) tmp[j]={INT_MIN,INT_MIN};
		for(int j=1;j<=min(m,Size[u]);j++){    //不要用填表法,會 TLE 
			for(int k=1;k<=min(m,Size[v]);k++){
				tmp[j+k]=max(tmp[j+k],{f[v][k].fi+f[u][j].fi+(f[v][k].se>0),f[u][j].se});
				tmp[j+k-1]=max(tmp[j+k-1],{f[v][k].fi+f[u][j].fi,f[v][k].se+f[u][j].se});
			}
		}
		for(int j=1;j<=Size[u]+Size[v];j++) f[u][j]=tmp[j];
		Size[u]+=Size[v];
	}
//	cout<<u<<':'<<'\n';
//	for(int i=1;i<=Size[u];i++){
//		cout<<i<<':'<<f[u][i].fi<<','<<f[u][i].se<<'\n';
//	}
}
void Init(){
	tot=0;;
	for(int i=1;i<=n;i++) head[i]=0,a[i]=0;
}
signed main(){
	T=read();
	while(T--){
		n=read(),m=read();
		Init();
		for(int i=1;i<=n;i++){
			ll b=read();
			a[i]-=b;
		}
		for(int i=1;i<=n;i++){
			ll w=read();
			a[i]+=w;
		}
		for(int i=1;i<n;i++){
			int u=read(),v=read();
			add(u,v),add(v,u);
		}
		dfs(1,0);
		printf("%d\n",f[1][m].fi+(f[1][m].se>0));
	}
	return 0;
}

38.CF1500D Tiles for Bathroom

注意到答案是單調的,一個大正方形裡的小正方形都滿足
為了避免重複,我們對每個右下角統計答案。

考慮計算出 \(f[i][j]\) 表示以 \((i,j)\) 為右下角,最大的合法正方形。

考慮從 \(f[i-1][j-1]\) 的繼承過來,同時會新增第 \(i\) 行和第 \(j\) 列的貢獻。

因為 \(q\) 很小,我們完全可以記錄對應的合法正方行裡,每個顏色出現的最遠的位置(這個距離定義為切比雪夫距離,即 \(\max(xi-xj,yi-yj)\) )。
所以為了方便更新,我們維護三個佇列:

  • \(left[i][j]\): \((i,j)\)左邊前 \(q+1\) 個顏色的位置(同一種顏色取切比雪夫距離最小的,下同)
  • \(up[i][j]\): \((i,j)\)上邊前 \(q+1\) 個顏色的位置
  • \(lu[i][j]\): \((i,j)\)左上方(即\(1 \le x \le i,1 \le y \le j\))所有的元素中,前 \(q+1\) 個顏色的位置。

\(left\)\(up\) 的更新很容易,只需要加進來 \((i,j)\) 即可。
看一下 \(lu[i][j]\):只需要從 \(left[i][j-1]\),\(up[i-1][j]\),\(lu[i-1][j-1]\),\((i,j)\) 中不斷取最小的即可,
維護時要滿足三個佇列內的元素與 \((i,j)\) 的切比雪夫距離始終單調遞增。

計算 \(f[i][j]\) 時,如果 \(lu[i][j]\) 的個數不超過 \(q\),那就是 \(f[i][j]=min(i,j)\)
否則 \(f[i][j]\)\(lu[i][j]\) 的第 \(q+1\) 個顏色與 \((i,j)\) 的切比雪夫距離 \(-1\)
時間複雜度:\(O(n^2 \times q)\)

注意到這題卡空間,所以存位置的時候不要用 \(pair\) , 把它變成一個數。
程式碼有點醜。

code

#include<bits/stdc++.h>
#define PII pair<int,int>
#define fi first
#define se second
using namespace std;
const int N=1505;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,q,a[N][N];
int ans[N]; 
int PairToNum(PII u){   //把座標 (x,y) 變成數字 
	return (u.fi-1)*n+(u.se-1); 
}
PII NumToPair(int pos){ //把數字 變成 座標
	return {pos/n+1,pos%n+1};
}
struct Queue{
	int q[12]; 
	int head=0,tail=-1;
	bool empty(){return tail<head;}
	void push(PII x){
		q[++tail]=PairToNum(x);
	}
	void pop(){
		++head;
	}
	void pop_back(){
		--tail;
	}
	PII front(){
		if(empty()) return {0,0}; 
		return NumToPair(q[head]);
	}
	int size(){return tail-head+1;};
}L[N][N],U[N][N],LU[N][N];
int calc(PII u,PII v){   //計算切比雪夫距離(真正的是不用+1的,這裡要計算正方形邊長所以加一))
	return max(abs(u.fi-v.fi),abs(u.se-v.se))+1;
}

bool flag[N*N];
signed main(){
	n=read(),q=read();
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			a[i][j]=read();
		}
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			L[i][j].push({i,j});
			for(int k=L[i][j-1].head;k<=L[i][j-1].tail;k++){
				PII u=NumToPair(L[i][j-1].q[k]);
				if(a[u.fi][u.se]==a[i][j]) continue;;
				L[i][j].push(u);
			}
			if(L[i][j].size()>q+1) L[i][j].pop_back();
			
			U[i][j].push({i,j});
			for(int k=U[i-1][j].head;k<=U[i-1][j].tail;k++){
				PII u=NumToPair(U[i-1][j].q[k]);
				if(a[u.fi][u.se]==a[i][j]) continue;;
				U[i][j].push(u);
			}
			if(U[i][j].size()>q+1) U[i][j].pop_back();
			
			LU[i][j].push({i,j});
			flag[a[i][j]]=true;   //記錄這種顏色是否出現過 
			int t1=L[i][j-1].head,t2=U[i-1][j].head,t3=LU[i-1][j-1].head;
			while(LU[i][j].size()<=q && (!L[i][j-1].empty() || !U[i-1][j].empty() || !LU[i-1][j-1].empty())){
				PII u=L[i][j-1].front(),v=U[i-1][j].front(),w=LU[i-1][j-1].front();
				 
				int d1=calc(u,{i,j}),d2=calc(v,{i,j}),d3=calc(w,{i,j}),mind=min({d1,d2,d3});   //取最小的那一個 
				if(mind==d1 && !L[i][j-1].empty()){
					L[i][j-1].pop();
					if(flag[ a[u.fi][u.se] ]) continue;  //出現過就忽略了 
					LU[i][j].push(u);
					flag[ a[u.fi][u.se] ]=true;	
				} 
				else if(mind==d2 && !U[i-1][j].empty()){
					U[i-1][j].pop();
					if(flag[ a[v.fi][v.se] ]) continue;
					LU[i][j].push(v);
					flag[ a[v.fi][v.se] ]=true;
				} 
				else{
					LU[i-1][j-1].pop();
					if(flag[ a[w.fi][w.se] ])continue;
					LU[i][j].push(w);
					flag[ a[w.fi][w.se] ]=true;
				}
			}
			L[i][j-1].head=t1,U[i-1][j].head=t2,LU[i-1][j-1].head=t3;
			for(int k=LU[i][j].head;k<=LU[i][j].tail;k++){
				PII u=NumToPair( LU[i][j].q[k] );
				flag[a[u.fi][u.se]]=false;
			} 
			
			int tmp=min(i,j);
			if(LU[i][j].size()==q+1) tmp=min(tmp,calc( NumToPair(LU[i][j].q[LU[i][j].tail]) , {i,j} )-1); 
			ans[tmp]++;
		} 
	}
	
	for(int i=n;i>=1;i--) ans[i]+=ans[i+1];
	for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
	return 0;
}

39.CF1276D Tree Elimination

神仙分類討論樹形DP題,按照連向父親的邊什麼時候被刪的分類討論一下,因為 LaTex 打起來太煩了,所以具體看題解區的吧。

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=2e5+5,mod=998244353;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n;
vector<int> G[N];  //vector存邊天然優勢:編號大的在後面 
int f[N][4],pre[N],suf[N];
void dfs(int u,int fa){
	bool flag=false;  //編號是否比 (u,fa) 大 
	f[u][1]=f[u][3]=1;
	for(int v:G[u]){
		if(v==fa){
			flag=true;
			continue;
		} 
		dfs(v,u);
		(f[u][3]*=(f[v][0]+f[v][1])%mod)%=mod;
		if(!flag){
			(f[u][1]*=(f[v][0]+f[v][1])%mod)%=mod;  //此時 u 還在 
		}
		else{
			(f[u][1]*=(f[v][0]+f[v][2]+f[v][3])%mod)%=mod;   //此時 u 已經沒了
		}
	}
	pre[0]=1;
	for(int i=1;i<=G[u].size();i++){
		int v=G[u][i-1];
		if(v==fa){
			pre[i]=pre[i-1];
			continue;
		}
		(pre[i]=pre[i-1]*(f[v][0]+f[v][1])%mod)%=mod;
	}
	suf[G[u].size()+1]=1;
	for(int i=G[u].size();i>=1;i--){
		int v=G[u][i-1];
		if(v==fa){
			suf[i]=suf[i+1];
			continue;
		}
		(suf[i]=suf[i+1]*(f[v][0]+f[v][2]+f[v][3])%mod)%=mod;
	}
	f[u][0]=f[u][2]=0;
	flag=false;
	for(int i=1;i<=G[u].size();i++){
		int v=G[u][i-1];
		if(v==fa){
			flag=true;
			continue;
		} 
		if(!flag){
			(f[u][0] += (f[v][2]+f[v][3])%mod * pre[i-1]%mod * suf[i+1]%mod)%=mod; 
		}
		else{
			(f[u][2] += (f[v][2]+f[v][3])%mod * pre[i-1]%mod * suf[i+1]%mod)%=mod;  
		}
	}
	
}
signed main(){
	n=read();
	for(int i=1;i<n;i++){
		int u=read(),v=read();
		G[u].push_back(v),G[v].push_back(u);
	}
	dfs(1,0);
	printf("%lld\n",(f[1][0]+f[1][2]+f[1][3])%mod);
	return 0;
}

40.P1081 [NOIP2012 提高組] 開車旅行

經典倍增最佳化DP題。

  1. 預處理:
    \(a[i],b[i]\) 分別表示 \(A/B\) 開車從 \(i\) 開始,下一個到達的城市。
    用 multiset 維護即可,不贅述

  2. DP:

  • \(f[i][j][k]\) 表示行駛 \(2^i\) 天,從城市 \(j\) 開始,\(k\) 先開車會到的城市。
  • \(sa[i][j][k]\) 表示行駛 \(2^i\) 天,從城市 \(j\) 開始,\(k\) 先開車,小A開的距離。
  • \(sb[i][j][k]\) 表示行駛 \(2^i\) 天,從城市 \(j\) 開始,\(k\) 先開車,小B開的距離。
    注意當 \(i=0\) 時,\(2^0=1\)為奇數,轉移時要注意開車的人會變。其餘就正常的轉移
  1. calc函式:
    \(calc(s,x)\) 表示從 \(s\) 開始,最多行駛 \(x\),小A和小B分別會行駛的距離,倍增即可

  2. 回答詢問:
    詢問一:只會問一次,暴力列舉起點。
    詢問二:直接輸出 \(calc(si,xi)\)

code

#include<bits/stdc++.h>
#define PII pair<int,int>
#define fi first
#define se second
#define int long long 
using namespace std;
const int N=1e5+5,M=(1<<17)+5,inf=2e14;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,h[N];

int a[N],b[N];
multiset<PII> s;
multiset<PII>::iterator it1,it2; 
int dist(int x,int y){
	if(x==0||y==0) return inf;
	return abs(h[x]-h[y]);
}
int u;
bool cmp1(PII x,PII y){
	if(dist(x.se,u)==dist(y.se,u)) return x.fi<y.fi;
	return dist(x.se,u)<dist(y.se,u);
}
void Init(){
//	return;
	s.insert({inf,0}),s.insert({inf,0}),s.insert({-inf,0}),s.insert({-inf,0});
	//防止越界 
	for(int i=n;i>=1;i--){
		PII tmp[6];
		s.insert({h[i],i});
		it1=it2=s.find({h[i],i});
		--it1; tmp[1]=*it1;
		--it1; tmp[2]=*it1;
		++it2; tmp[3]=*it2;
		++it2; tmp[4]=*it2;
		u=i;
		sort(tmp+1,tmp+4+1,cmp1);
		a[i]=tmp[2].se,b[i]=tmp[1].se;
	}
}


int f[20][N][2],sa[20][N][2],sb[20][N][2];
void DP(){
	for(int i=1;i<=n;i++){
		f[0][i][0]=a[i],f[0][i][1]=b[i];
		sa[0][i][0]=dist(i,a[i]),sa[0][i][1]=0;
		sb[0][i][1]=dist(i,b[i]),sb[0][i][0]=0;
	}
	for(int i=1;i<=17;i++){
		for(int j=1;j<=n;j++){
			for(int k=0;k<=1;k++){
				if(i==1){
					f[i][j][k]=f[i-1][ f[i-1][j][k] ][1-k];
					sa[i][j][k]=sa[i-1][j][k] + sa[i-1][ f[i-1][j][k] ][1-k];
					sb[i][j][k]=sb[i-1][j][k] + sb[i-1][ f[i-1][j][k] ][1-k];
				}
				else{
					f[i][j][k]=f[i-1][ f[i-1][j][k] ][k];
					sa[i][j][k]=sa[i-1][j][k] + sa[i-1][ f[i-1][j][k] ][k];
					sb[i][j][k]=sb[i-1][j][k] + sb[i-1][ f[i-1][j][k] ][k];
				}
			}
		}
	}
}

PII calc(int s,int x){
	int suma=0,sumb=0,sum=0;
	for(int i=17;i>=0;i--){
		if(f[i][s][0]&&(sum+sa[i][s][0]+sb[i][s][0])<=x){
			suma+=sa[i][s][0],sumb+=sb[i][s][0],sum=suma+sumb;
			s=f[i][s][0];
		}
	}
	return {suma,sumb};
}

struct frac{
	int a,b,id;
};
bool cmp(frac x,frac y){
	if(x.b==y.b&&x.b==0) return h[x.id]>h[y.id];
	if(y.b==0) return true;
	if(x.b==0) return false;
	if(x.a*y.b==y.a*x.b) return h[x.id]>h[y.id];
	return x.a*y.b<y.a*x.b;
}
void solve1(){
	int x0=read();
	frac ming={1,0,0};
	for(int i=1;i<=n;i++){
		int s1=calc(i,x0).fi,s2=calc(i,x0).se;
		if(cmp({s1,s2,i},ming)) ming={s1,s2,i};
	}	
	printf("%lld\n",ming.id);
}

void solve2(){
	int T=read();
	while(T--){
		int s=read(),x=read();
		printf("%lld %lld\n",calc(s,x).fi,calc(s,x).se);
	}
}
signed main(){
	n=read();
	h[0]=-inf;
	for(int i=1;i<=n;i++) h[i]=read();
	Init();
	DP();
	solve1();
	solve2();
	return 0;
}

41.P9197 [JOI Open 2016] 摩天大樓

其實這個在動態規劃題單一應該就要寫了,但是太懶了,所以拖到現在。

套路和 [CEOI2016] kangaroo 有點類似(在第一個題單裡),所以建議先看那題。
那我們先列出狀態: \(f[i][j][k][0/1][0/1]\) 表示從大到小放到第 \(i\) 個數,一共有 \(j\) 個連續段,題目裡的式子計算結果為 \(k\),放/沒放第一個,放/沒放最後一個的方案數。
但這樣如果我們每一次新放進來一個數,只是計算他和他兩邊的數新增的貢獻,我們還需要記錄整個序列的哪些位置填了哪些數,才能轉移動 \(k\),但這樣狀態就炸了。所以我們考慮轉換一下計算 \(|f_1-f_2| + |f_2-f_3| +...+ |f_{N-1}-f_N|\) 的計算方法。

假如最後是這麼個填數方案,橫座標是下標,縱座標是數值。

所以當我們從大到小填到第四個數的時候(即圖中的\(f_6\)),我們按照原始的方法其實只計算了圖中綠色的線段的值。

這就使得我們再加進第五個點的時候很不好計算多出來的答案,所以我們計算答案的方式變為:
當新放進來一個數 \(a_i\) 時,假設現在的段數是 \(j\),那麼把答案累加上 \((a_{i-1}-a_i) \times (2j)\),意思是每一段自動在兩側延申 \((a_{i-1}-a_i)\),也可以看作是提前計算貢獻

這樣當我們從大到小填到第四個數的時候,我們其實就計算了圖中藍色部分的線段了。

但這樣還是會有個小問題,就是在加第\(5\)個點時,按照上述演算法,多出來的答案是圖中紅色加上黃色線段。
但其實黃色線段是沒有的。

解決辦法也比較簡單,在開頭和結尾已經放了的情況下轉移特判一下即可。

具體細節見程式碼。

code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5,mod=1e9+7;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,L,a[105];
int f[105][105][1005][2][2];
/*
	f[i][j][k][0/1][0/1]表示從大到小放到第 i 個數,一共有j個連續段,題目裡的式子計算結果為 k,放/沒放第一個,放/沒放最後一個 
*/
signed main(){
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	n=read(),L=read();
	for(int i=1;i<=n;i++) a[i]=read();
	if(n==1){
		cout<<1<<'\n';
		return 0;   // 此時它又是開頭,又是結尾 
	}
	sort(a+1,a+n+1,greater<int>());
	
	f[0][0][0][0][0]=1; 
	for(int i=0;i<=n;i++){
		for(int j=0;j<=i;j++){
			for(int k=0;k<=L;k++){
				int sum;  //新增貢獻
				 
				//從f[i][j][k][0][0]轉移
				sum=k + 2 * j * (a[i] - a[i+1]);
				if(sum<=L&&sum>=0){
					//1.i+1放開頭
					( f[i + 1][j + 1][sum][1][0] += f[i][j][k][0][0] ) %= mod;   //自成一段 
					( f[i + 1][j][sum][1][0] += f[i][j][k][0][0] ) %= mod;     //粘在一段前面 
					//2.i+1放結尾
					( f[i + 1][j + 1][sum][0][1] += f[i][j][k][0][0] ) %= mod;   //自成一段 
					( f[i + 1][j][sum][0][1] += f[i][j][k][0][0] ) %= mod;     //粘在一段後面 
					//3.i+1放中間
		 			( f[i + 1][j + 1][sum][0][0] += (j + 1) * f[i][j][k][0][0] % mod ) %= mod; //自成一段 
					( f[i + 1][j][sum][0][0] += 2 * j * f[i][j][k][0][0] % mod ) %= mod;  //粘在一段前面/後面
					( f[i + 1][j - 1][sum][0][0] += (j - 1) * f[i][j][k][0][0] % mod) %= mod;  //把兩段粘在一起 
				}
				
				//從f[i][j][k][1][0]轉移 
				sum=k + (2 * j - 1) * (a[i] - a[i+1]);
				if(sum<=L&&sum>=0){
					//1.i+1放結尾
					( f[i + 1][j + 1][sum][1][1] += f[i][j][k][1][0] ) %= mod;   //自成一段 
					( f[i + 1][j][sum][1][1] += f[i][j][k][1][0] ) %= mod;     //粘在一段後面
					//2.i+1放中間,不能放開頭了 
		 			( f[i + 1][j + 1][sum][1][0] += j * f[i][j][k][1][0] % mod ) %= mod; //自成一段 
					( f[i + 1][j][sum][1][0] += (2 * j - 1) * f[i][j][k][1][0] % mod ) %= mod;  //粘在一段前面/後面
					( f[i + 1][j - 1][sum][1][0] += (j - 1) * f[i][j][k][1][0] % mod) %= mod;  //把兩段粘在一起
				}
				
				//從f[i][j][k][0][1]轉移
				sum=k + (2 * j - 1) * (a[i] - a[i+1]);
				if(sum<=L&&sum>=0){
					//1.i+1放開頭
					( f[i + 1][j + 1][sum][1][1] += f[i][j][k][0][1] ) %= mod;   //自成一段 
					( f[i + 1][j][sum][1][1] += f[i][j][k][0][1] ) %= mod;     //粘在一段前面 
					//2.i+1放中間,不能放結尾了 
		 			( f[i + 1][j + 1][sum][0][1] += j * f[i][j][k][0][1] % mod ) %= mod; //自成一段 
					( f[i + 1][j][sum][0][1] += (2 * j - 1) * f[i][j][k][0][1] % mod ) %= mod;  //粘在一段前面/後面
					( f[i + 1][j - 1][sum][0][1] += (j - 1) * f[i][j][k][0][1] % mod) %= mod;  //把兩段粘在一起	
				}
				
				//從f[i][j][1][1]轉移
				sum=k + (2 * j - 2) * (a[i] - a[i+1]);
				if(sum<=L&&sum>=0){
					//1.i+1放中間,不能放開頭和結尾了 
		 			( f[i + 1][j + 1][sum][1][1] += (j - 1) * f[i][j][k][1][1] % mod ) %= mod; //自成一段 
					( f[i + 1][j][sum][1][1] += (2 * j - 2) * f[i][j][k][1][1] % mod ) %= mod;  //粘在一段前面/後面
					( f[i + 1][j - 1][sum][1][1] += (j - 1) * f[i][j][k][1][1] % mod) %= mod;  //把兩段粘在一起	
				}
			}
		}
	} 
	
	int ans=0;
	for(int i=0;i<=L;i++){
		(ans += f[n][1][i][1][1]) %= mod;
	}
	printf("%lld\n",ans);
	return 0;
}




42.Count The Repetitions*

\([S2,M]=[s2,n2*M]\)
找到最大的 \(M\) 也就是找到最大的 \(M'\) 使得 \([S2,M']\)\([s1,n1]\) 的子序列。答案那就是 \(\lfloor \frac {M'} {n2} \rfloor\)

無解的判定:如果 \(s2\) 中的字元沒有全部在 \(s1\) 中出現就無解。否則至多重複 \(s1\) \(len2\) 次就可以得到一個 \(s2\) (\(len2\)\(s2\) 的長度)。

預處理:\(g[i]\) 表示從 \(s1\) 的第 \(i\) 位開始,至少接需要多少個字元才能湊出一個 \(s2\),注意不是接多少個 \(s1\),而是字元,\(O(len^3)\) 暴力即可

\(f[i][s]\) 表示從 \(s1\) 的第 \(s\) 個開始,湊出 \(2^i\)\(s2\) 需要多少個字元,轉移顯然。

倍增最佳化求答案的過程即可。

code


#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e5+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
char s1[105],s2[105];
int n1,n2,len1,len2;
int g[105],f[40][105];
bool stater;
void Init(){
	memset(g,0x3f,sizeof g);
	memset(f,0x3f,sizeof f);
	len1=strlen(s1),len2=strlen(s2);
	stater=true;
	string s;
	for(int i=1;i<=len2;i++) s=s+s1;
	for(int i=0;i<len1;i++){
		int k=0;
		for(int j=i,cnt=1;j<s.size();j++,cnt++){
			if(s[j]==s2[k]){
				k++;
				if(k==len2){
					g[i]=cnt;
					break;
				}
			}
		}
		if(g[i]>s.size()){
			stater=false;
			return;
		}
		else f[0][i]=g[i];
	}
	for(int t=1;t<=30;t++){
		for(int i=0;i<len1;i++){
			f[t][i]=f[t-1][i]+f[t-1][(i+f[t-1][i])%len1];
		}
	}
}
void work(){
	if(!stater){
		puts("0");
		return;
	}
	int maxn=len1*n1,sum=0,res=0,pos=0;
	for(int i=30;i>=0;i--){
		if(f[i][pos]+sum<=maxn){
			sum+=f[i][pos];
			pos=(pos+f[i][pos])%len1;
			res+=pow(2,i);
		}
	}
	printf("%lld\n",res/n2);
}
signed main(){
	 while(scanf("%s %lld\n%s %lld",s2,&n2,s1,&n1)==4){
		Init();
		work();
	}
	return 0;
}

43.CF1788E Sum Over Zero

\(f[i]\):表示區間 \([1,i]\) 上的最大值 (一定要選 \(i\))。
\(pre[i]\):表示 \(max{f[0],f[2],...,f[i]}\)
\(s[i]=a1+a2+...+ai\)

轉移:\(f[i]=max_{0 \le j \le i-1,si-sj \ge 0}({i-j+pre[j]})\)

\(f[i]\) 轉移的最佳化:在每個 \(s[j]\) 上記錄最大的 \(pre[j]-j\) , 轉移時線上段樹上查詢 \((-inf,si]\) 的最大值。

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=2e5+5,MAXN=2e14;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,a[N];
int s[N],pre[N],f[N];
int dis[N],m;
int Dis(int x){
	return lower_bound(dis+1,dis+m+1,x)-dis; 
}
struct node{
	int l,r,maxn;
};
struct SegmentTree{
	node t[10000005];
	void pushup(int p){
		t[p].maxn=max(t[p<<1].maxn,t[p<<1|1].maxn);
	}
	void build(int p,int l,int r){
		t[p].l=l,t[p].r=r,t[p].maxn=LLONG_MIN;
		if(l==r) return;
		int mid=(l+r)>>1;
		build(p<<1,l,mid);
		build(p<<1|1,mid+1,r);
		pushup(p);
	}
	void change(int p,int x,int val){
		if(t[p].l==t[p].r){
			t[p].maxn=max(t[p].maxn,val);
			return;
		}
		int mid=(t[p].l+t[p].r)>>1;
		if(x<=mid){
			change(p<<1,x,val);
		}
		else{
			change(p<<1|1,x,val);
		}
		pushup(p);
	}
	int ask(int p,int l,int r){
		if(l<=t[p].l&&t[p].r<=r){
			return t[p].maxn;
		}	
		int mid=(t[p].l+t[p].r)>>1,res=LLONG_MIN;
		if(l<=mid) res=max(res,ask(p<<1,l,r));
		if(r>mid) res=max(res,ask(p<<1|1,l,r));
		return res;
	}
}Seg;
signed main(){
	int ming=0;
	n=read();
	dis[n+1]=0;
	for(int i=1;i<=n;i++) a[i]=read(),s[i]=s[i-1]+a[i],dis[i]=s[i],ming=min(ming,s[i]);
	sort(dis+1,dis+n+1+1);
	m=unique(dis+1,dis+n+1+1)-dis-1; 
	
	Seg.build(1,1,m);
	f[0]=0;
	pre[0]=0;
	Seg.change(1,Dis(s[0]),pre[0]);
	for(int i=1;i<=n;i++){
		f[i]=Seg.ask(1,Dis(ming),Dis(s[i]))+i;
		pre[i]=max(pre[i-1],f[i]);
		Seg.change(1,Dis(s[i]),pre[i]-i);
	}
	printf("%lld\n",pre[n]);
	return 0;
}

44.CF1799D2 Hot Start Up (hard version)

壓縮狀態最佳化DP 。

\(f[i][x][y]\) 表示處理完第 \(i\) 個程式,目前第一個 CPU 裡執行的程式種類是 \(x\),第二個裡執行的程式種類是 \(y\)
因為 \(x,y\) 中一定有一個是 \(ai\),考慮把第一維去掉,那麼有轉移(很明顯能用 \(hot\) 就不用 \(cold\)):

  1. \(f[x][y]+cold[ai] \to f[ai][y]\)
  2. \(f[x][y]+cold[ai] \to f[x][ai]\)
  3. \(f[x][ai]+hot[ai] \to f[x][ai]\)
  4. \(f[ai][x]+hot[ai] \to f[ai][x]\)

直接跑是 \(O(n\times k^2)\)

我們會發現每次轉移時,都只會轉移到 \(f[ai][x]\)\(f[x][ai]\)。同理在進行轉移的時候也只有 \(f[a[i-1]][x]\)\(f[x][a[i-1]]\) 中會有值。只需要列舉 \(x\) ,進行轉移即可。

因為兩個 CPU 本質相同,所以 \(f[x][y]\) 顯然等於\(f[y][x]\),即轉移最佳化成: (預設 \(a[i-1] \ne a[i]\),如果相等直接全部加 \(hot[ai]\) 即可)

  1. \(f[a[i-1]][x]+cold[ai] \to f[a[i-1]][ai]\) (此時如果 \(x=ai\) 的話,肯定不如第3條優)
  2. \(f[a[i-1]][x]+cold[ai] \to f[ai][x]\)
  3. \(f[a[i-1]][ai]+hot[ai] \to f[a[i-1]][ai]\)

可以透過簡單版。

還是因為 \(f[x][y]=f[y][x]\),所以轉移到 \(f[ai][x]\) 和 轉移到 \(f[x][ai]\) 是一樣的,所以我們考慮進一步壓縮狀態用 \(f[x]\) 代替 \(f[ai][x]\),那麼上面那三個轉移式子就最佳化成:

  • 如果 \(a[i-1] \ne ai\)
  1. \(f[x]+cold[ai] \to f[a[i-1]]\)
  2. \(f[x]+cold[ai] \to f[x]\)
  3. \(f[ai]+hot[ai] \to f[a[i-1]]\)

分別進行最佳化:

  1. 轉移到的目標是定的,直接維護全域性最小值進行轉移即可。
  2. 相當於每個點都加一個 \(cold[ai]\),我們用 \(add\) 記錄一下全域性累加標記即可。
  3. 下標都給定了,直接轉移。
  • 如果 \(a[i-1]=ai\),直接 \(add+=hot[ai]\)

幾個注意:

  1. 如果進行了轉移1或3,那此時是不用加上轉移2的 \(cold[ai]\) 的,要先減掉 \(cold[ai]\)
  2. 維護新的全域性最小值時全部重新跑一遍肯定不太對,我們會發現這三個轉移中只有轉移二會轉移到 \(f[x]\),但是 \(f[x]+cold[ai]\) 不僅會轉移給 \(f[x]\),也會轉移給 \(f[a[i-1]]\),而 \(f[a[i-1]]\) 還可以透過 \(f[ai]+hot[ai]\) 轉移,所以 \(f[a[i-1]]\) 一定是全域性最小值,只要用它更新即可。
    3.一開始 \(f[0]=0\), 其餘全是 \(inf\)

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=3e5+5,inf=3e14+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int T;
int n,k,a[N],cold[N],hot[N],f[N];
void work(){
	n=read(),k=read();
	for(int i=1;i<=n;i++) a[i]=read();
	for(int i=1;i<=k;i++) cold[i]=read(),f[i]=inf;
	for(int i=1;i<=k;i++) hot[i]=read();
	f[0]=0;
	int add=0,ming=0;
	for(int i=1;i<=n;i++){
		if(a[i]!=a[i-1]){
			add+=cold[a[i]];
			f[a[i-1]]=min({f[a[i-1]] , ming+cold[a[i]]-cold[a[i]] , f[a[i]]+hot[a[i]]-cold[a[i]]});
			ming=f[a[i-1]];
		}
		else add+=hot[a[i]];
	}
	printf("%lld\n",ming+add);
}
signed main(){
	T=read();
	while(T--) work();
	return 0;
}

45.CF1304F2 Animal Observation (hard version)

題意簡化:給定一個 \(n\times m\) 的矩形,每一行選擇一個點,並以這個點為左上角框出一個 \(2\times k\) 的小矩形,求所有小矩形的並所覆蓋的數字之和的最大值。

\(m'=m-k+1\),顯然每個點的縱座標不會超過 \(m'\)

\(f[i][j]\) 表示第 \(i\) 行選第 \(j\) 個點,前 \(i\) 行的最大值。
如果可以重複,那 DP 方程容易得出:
\(f[i][j]=max_{1\le x\le m'}(f[i-1][x]+pre[i][x+k-1]-pre[i][x-1]) + pre[i][j+k-1] - pre[i][j-1]\), \(pre[i]\) 是第 \(i\) 行的字首和。
前面一項容易維護,從而 \(O(1)\) 轉移,總時間複雜度 \(O(n^2)\)

考慮去掉重複貢獻。
\(f[i][j]\) 表示第 \(i\) 行選第 \(j\) 個點,前 \(i\) 行的最大值(不能重複貢獻)。
又設 \(g[i][j]=f[i][j]+pre[i+1][j+k-1]-pre[i+1][j-1]\)
則轉移方程可以寫成:
\(f[i][j]=max_{1\le x \le m'}{g[i-1][x]} + pre[i][j+k-1] - pre[i][j-1] - 重複貢獻的部分\)
考慮把重複貢獻的部分在 max 裡就去掉,一個點 \(a[i][y] (j\le y\le j+k-1)\) 重複貢獻在:
\(g[i-1][y-k+1],...,g[i-1][y-1],g[i-1][y]\)
只需要把他們全部減去 \(a[i][y]\) 即可,支援區間減,區間查最大值的是什麼? 線段樹。轉移完之後再加回去。

但這樣是 \(O(n \times m \times k \times \log m)\),因為要對每個 \(j<=y<=j+k-1\) 都做一遍區間減。但是當 \(j\)\(j\) 變到 \(j+1\),其實只有 \(j\) 這個位置不再被重複貢獻,\(j+1+k-1\) 這個位置新加進來,所以就好了。
所以 \(O(n\times m\times \log m)\)

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=2e4+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,k,a[55][N],pre[55][N];
int f[55][N],g[N];
struct node{
	int l,r,maxn,add;
	void tag(int d){
		maxn+=d;
		add+=d;
	}
};
struct SegmentTree{
	node t[N<<2];
	void pushup(int p){
		t[p].maxn=max(t[p<<1].maxn,t[p<<1|1].maxn); 
	}
	void spread(int p){
		if(t[p].add){
			t[p<<1].tag(t[p].add);
			t[p<<1|1].tag(t[p].add);
			t[p].add=0;
		}
	}
	void build(int p,int l,int r){
		t[p].l=l,t[p].r=r,t[p].add=0;    //add也要清空 
		if(l==r){
			t[p].maxn=g[l];
			return;
		}	
		int mid=(l+r)>>1;
		build(p<<1,l,mid);
		build(p<<1|1,mid+1,r);
		pushup(p);
	} 
	void change(int p,int l,int r,int d){
		if(l<=t[p].l&&t[p].r<=r){
			t[p].tag(d);
			return;
		}
		spread(p);
		int mid=(t[p].l+t[p].r)>>1;
		if(l<=mid) change(p<<1,l,r,d);
		if(r>mid) change(p<<1|1,l,r,d);
		pushup(p);
	}
}Seg;

signed main(){
	n=read(),m=read(),k=read();
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			a[i][j]=read();
			pre[i][j]=pre[i][j-1]+a[i][j];
		}
	}
	
	int m1=m-k+1;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m1;j++){
			if(i!=1){
				if(j==1){
					for(int y=j;y<=j+k-1;y++) Seg.change(1,1,y,-a[i][y]);
				}
				else{
					Seg.change(1,max(1ll,j-1-k+1),j-1,a[i][j-1]);
					Seg.change(1,j,j+k-1,-a[i][j+k-1]);
				}
			}
			f[i][j]=Seg.t[1].maxn+pre[i][j+k-1]-pre[i][j-1];
			g[j]=f[i][j]+pre[i+1][j+k-1]-pre[i+1][j-1];
		}
		Seg.build(1,1,m1);
	}
	int ans=0;
	for(int i=1;i<=m1;i++) ans=max(ans,f[n][i]);
	printf("%lld\n",ans);
	return 0;
}

相關文章