CSP-J/S2019試題選做


S D1T2 括號樹

\(f[u]\)表示根到\(u\)的路徑上有多少子串是合法括號串。(即題目里的\(k_u\),此變量名缺乏個性,故換之)

從根向每個節點dfs,容易求出\(c[u]\):表示從根到\(u\)的路徑上,我們能匹配則匹配,最后剩下多少個待匹配的左括號。 例如如下\(s_u\)對應的\(c[u]\)

  • ((() \(c[u]=2\)

  • (())( \(c[u]=1\)

  • (()() \(c[u]=1\)

  • ()))( \(c[u]=1\)

  • (()()) \(c[u]=0\)

如果\(u\)節點上括號為)\(c[u]\geq 0\),說明可以以\(u\)節點為結尾,形成一個新的合法子序列。我們大膽猜想,滿足上述兩個條件時,\(f[u]=f[fa(u)]+1\),否則\(f[u]=f[fa(u)]\)。 可惜過不了大樣例。 仔細分析一下發現我們漏算了很多子串。

如:對於\(s_u=\)(()(),因為\(f[4]=1\),所以我們會認為\(f[5]=2\),但其實\(f[5]\)應該等於\(3\),我們漏算了\([2,5]\)這個合法子串。 事實上,我們這樣做只計算出了以\(u\)結尾的最短合法子串。問題就出在,當序列結尾形成一個新的合法子串時,它可能和前面原本已經閉合的的子串拼在一起,結合形成“並列式”子串。 極端的例子如(()()()()\([8,9]\)這個子串,可以和\([6,7]\)拼,也可以和\([4,7]\)拼,甚至可以和\([2,7]\)拼在一起,分別構成新的並列式子串。

我們稱:在同一個括號中的、並列出現的、合法的子串為同一層里的子串。如:(()()( () )) ()中,\([2,3],[4,5],[6,9]\)在同一層,\([1,10],[11,12]\)在同一層。

發現兩個合法子串在同一層的條件是:結尾位置的\(c[i]\)相同,且兩個子串中間沒有經過過一個\(c[x]<c[i]\)的部分。如:(()()) (())里的\([2,3]\)\([8,9]\)就不在同一層,因為它們是\(c[i]=1\),但中間經過了一個\(c[6]=0<1\)

如果子串結尾位置為\(i\),我們稱\(c[i]=k\)的子串位於第\(k\)層。

那么對每個\(u\),我們要求出,從根到\(u\)的路徑上,每一層有多少個並列的合法子串。這樣,我們就能知道\(c[u]\)這一層有多少合法子串能和“以\(u\)結尾的最短合法子串”拼接。

看起來這需要一個二維的數組來記錄。但我們發現,從“以\(fa(u)\)結尾的串里每一層有多少並列的合法子串”,到“以\(u\)結尾的串里每一層有多少並列的合法子串”,這當中的修改是很小且很特殊的:當\(u\)上是)時,\(c[u]\)這一層會多一個並列的合法子串,而\(c[i]>c[u]\)的這些層,它們的並列合法子串數將清零,這是根據我們前面分析出的“兩個合法子串在同一層的條件”中要求它們之間不能存在一個\(c[x]<c[i]\),現在\(u\)就是這個\(x\)了。

先從父親處繼承,然后單點\(+1\),並把這個點的后綴清零。主席樹可以實現。

時間、空間復雜度:\(O(n\log n)\)

參考代碼:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN=5e5+5;
struct EDGE{int nxt,to;}edge[MAXN];
int head[MAXN],tot;
inline void add_edge(int u,int v){edge[++tot].nxt=head[u];edge[tot].to=v;head[u]=tot;}
int n,rt[MAXN],ls[MAXN*30],rs[MAXN*30],val[MAXN*30],cnt;
void modify(int &x,int y,int l,int r,int pos){
	x=++cnt;val[x]=val[y];ls[x]=ls[y];rs[x]=rs[y];
	val[x]++;
	if(l==r)return;
	int mid=(l+r)>>1;
	if(pos<=mid)modify(ls[x],ls[y],l,mid,pos),rs[x]=0;
	else modify(rs[x],rs[y],mid+1,r,pos);
}
int query(int x,int l,int r,int pos){
	if(!x)return 0;
	if(l==r)return val[x];
	int mid=(l+r)>>1;
	if(pos<=mid)return query(ls[x],l,mid,pos);
	else return query(rs[x],mid+1,r,pos);
}
ll f[MAXN],c[MAXN];
char s[MAXN];
void dfs(int u){
	for(int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		f[v]=f[u];c[v]=c[u];rt[v]=rt[u];
		if(s[v]=='('){
			if(c[v]<0)c[v]=0,rt[v]=0;
			c[v]++;
		}
		else {
			c[v]--;
			if(c[v]>=0){
				f[v]++;
				f[v]+=query(rt[v],0,n,c[v]);
				modify(rt[v],rt[v],0,n,c[v]);
			}
		}
		dfs(v);
	}
}
int main(){
//	freopen("brackets.in","r",stdin);
//	freopen("brackets.out","w",stdout);
	scanf("%d%s",&n,s+1);
	for(int i=2,fa;i<=n;++i)scanf("%d",&fa),add_edge(fa,i);
	if(s[1]=='(')c[1]=1;else c[1]=-1;
	dfs(1);
	ll ans=0;
	for(int i=1;i<=n;++i)ans^=((ll)i*f[i]);
	//cout<<f[i]<<" ";cout<<endl;
	//
	cout<<ans<<endl;
	return 0;
}

S D1T3 樹上的數

我看了wqy大佬的題解並決定盜用他的圖(霧

首先考慮一條鏈的情況。

我們觀察鏈上某個節點\(u\)上的數字變動情況。可以發現無論我們先刪左邊還是先刪右邊,原來它上面的數字一定會被搬運到了其中一個方向,同時另一個方向一定又過來了一個數,此外還有一個數字反向經過了\(u\)

也就是下面這種情況(這里是先刪除右邊那條邊):

pic1

再考慮菊花,即圍繞着節點\(u\)有多條出邊的情況。如果把這些邊排成一圈依次拆掉,我們先會把\(u\)上的數挪到某個與它相鄰的節點上。然后從這個節點開始,按順時針順序,這一圈上每個數依次從\(u\)的某條邊進入,然后從順時針的下一條邊出去。最后某個數會進入\(u\)然后不再出去,這就是最終留在\(u\)上的數了。

如果畫出來會很優美:

pic2

因為是求字典序最小,顯然應該按數字\(1\)\(n\)貪心,每個數字都挪到它能挪到的編號最小的節點上。

不妨以當前數字所在的節點為根。我們做一遍dfs,判斷每個點是否能被挪到。節點\(u\)能被挪到,其實分為兩個部分的條件:

  1. 根到\(u\)的路徑上(不包括根和\(u\))每個節點都能夠作為“中轉節點”(圖中一進一出的那種);

  2. \(u\)自己能夠作為終點節點(圖中只進不出的那種)。

考慮條件1,對於每個\(v\),我們要判斷的其實是能否從根一路通暢地走入\(v\)的子樹。我們稱滿足這樣條件的\(v\)為“合法”。

  • 如果\(v\)的父親\(fa(v)\)不合法,則\(v\)肯定也不合法。

  • 如果\(fa(v)\)\(v\)的這條邊已經被以相同的方向使用過,則\(v\)不合法。(根據上圖,顯然每條邊最終都會被以兩個方向各走一遍,此處我們只要判斷\(fa(v)\)走向\(v\)的方向是否被走過)

  • 如果\(fa(v)\)不為根,我們從\(fa(fa(v))\)走到\(fa(v)\),再走向\(v\),相當於在\(fa(v)\)這個節點上一進一出。我們加入這一進一出的路徑后,如果恰好把\(fa(v)\)周圍所有經過它的路徑串成了一圈(首尾相接),但是\(fa(v)\)周圍仍然存在還沒走過的邊,則我們現在還不能走這一進一出。即\(v\)不合法。【注:這部分比較復雜,請結合圖和代碼仔細理解,具體的實現方式下面會講。】

考慮條件2,我們判斷一個點\(u\)能否作為終點,方法和判\(v\)類似。

  • 如果\(u\)已經存在一條只進不出的邊了,則\(u\)不能作為終點。(根據圖來看顯然每個點只能有一次只進不出的機會。根據實際含義來說就是,之前已經有別的數以\(u\)作為終點了,所以\(u\)不能作為當前數的終點)

  • 如果\(fa(u)\)\(u\)的這條邊已經被以相同的方向使用過,則\(u\)不能作為終點。

  • 如果加入\(fa(u)\)\(u\)的路徑后,\(u\)周圍的路徑會被串成一圈,但\(u\)周圍仍然存在還沒走過的邊,則\(u\)現在還不能作為終點。

這樣,對於每個數我們貪心地選編號最小的、能作為終點的點即可。選出后我們暴力更新這個點到根路徑上的信息。


下面講一些具體實現的問題:

判斷一條邊以哪個方向走過是比較容易的。需要注意的是:我們的根在不斷變化,所以不能以“是從父親走向兒子”還是“從兒子走向父親”來判斷。我是直接建了個二維的鄰接表,state[u][v]=u表示\(u,v\)間的邊從\(u\)\(v\)走過,state[u][v]=v表示\(u,v\)間的邊從\(v\)\(u\)走過,state[u][v]=0表示兩個方向都走過,state[u][v]=-1表示兩個方向都沒走過。

如何判斷加入一條路徑后,\(u\)周圍的所有路徑是否會被串成一圈?由於路徑是有方向的,若干路徑會串成一條鏈,我們記一條鏈的開頭為\(st\),結尾為\(ed\)。新加入的路徑如果是一進一出,那么我們要判斷,如果:進入的那頭的鏈的\(st\)\(u\)上數字移出的邊,走出的那頭的鏈的\(ed\)\(u\)上數字移入的邊,加入這一進一出后,就會把所有邊串成一條鏈了。

所謂\(u\)上數字移入、移出的邊是指:對於每個節點\(u\),都會有唯一的一條移出的邊,也會有唯一的一條移入的邊。如上圖中\(x\rightarrow 1\)就是移出,\(5\rightarrow x\)就是移入。【這部分請結合圖和代碼重點理解】

最開始時,\(u\)周圍的每條邊都是一條單獨的鏈。當我們加入一進一出的路徑,我們實際上是連起了兩條鏈。這個“連起”的過程,我們可以用並查集維護:對每個\(u\)都開一個並查集,維護它周圍的每條鏈的\(st\)\(ed\)。一進一出時,進的那條鏈提供合並后的\(st\),出的那條鏈提供合並后的\(ed\)

最后,請注意特判\(n=1\)時的情況,因為我們的程序中默認每個點都會移出去一次、移進來一次,而\(n=1\)時這種情況根本不會發生。

時間復雜度\(O(n^2\alpha)\),空間復雜度\(O(n^2)\)

參考代碼:

//problem:P5659
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fst first
#define scd second

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;

namespace Fread{
	const int MAXN=1<<20;
	char buffer[MAXN],*S,*T;
	inline char getchar(){
		if(S==T){
			T=(S=buffer)+fread(buffer,1,MAXN,stdin);
			if(S==T)return EOF;
		}
		return *S++;
	}
}
#ifdef ONLINE_JUDGE
	#define getchar Fread::getchar
#endif
inline int read(){
	int f=1,x=0;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
inline ll readll(){
	ll f=1,x=0;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}

const int MAXN=2005;
struct EDGE{int nxt,to;}edge[MAXN<<1];
int head[MAXN],tot;
inline void add_edge(int u,int v){edge[++tot].nxt=head[u],edge[tot].to=v,head[u]=tot;}
int n,p[MAXN],d_nu[MAXN],d_in[MAXN],d_out[MAXN],frm[MAXN],to[MAXN],state[MAXN][MAXN];
struct DSU{
	int fa[MAXN],st[MAXN],ed[MAXN],sz[MAXN];
	int get_fa(int x){
		return x==fa[x]?x:(fa[x]=get_fa(fa[x]));
	}
	void union_s(int frm,int to){
		int x=get_fa(frm),y=get_fa(to);
		if(x==y)return;
		if(sz[x]<sz[y])fa[x]=y,st[y]=st[x],sz[y]+=sz[x];
		else fa[y]=x,ed[x]=ed[y],sz[x]+=sz[y];
	}
	int get_st(int x){
		return st[get_fa(x)];
	}
	int get_ed(int x){
		return ed[get_fa(x)];
	}
	void Init(int x){
		fa[x]=st[x]=ed[x]=x;sz[x]=1;
	}
}b[MAXN];
/*
tips:
p[num] -> node
d_nu[u]: edge(from u) not used
stata[u][v]:
 - -1 not used
 - u u->v
 - v v->u
 - 0 u->v & v->u
*/
int Root,fa[MAXN];
bool flag[MAXN];
void dfs(int u){
	for(int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==fa[u])continue;
		fa[v]=u;
		flag[v]=1;
		//能否經過u,到達v
		if(!flag[u])flag[v]=0;
		else if(u!=Root){
			if(state[u][fa[u]]==0||state[u][fa[u]]==fa[u]||state[u][v]==0||state[u][v]==u)flag[v]=0;
			else if(flag[v]&&b[u].get_st(fa[u])==to[u]&&b[u].get_ed(v)==frm[u]&&d_in[u]+d_out[u]+d_nu[u]*2-2!=0)flag[v]=0;
			else if(flag[v]&&b[u].get_ed(v)==fa[u])flag[v]=0;
		}
		else{
			//assert(!to[u]);
			if(state[u][v]==0||state[u][v]==u)flag[v]=0;
			else if(flag[v]&&b[u].get_ed(v)==frm[u]&&d_in[u]+d_out[u]+d_nu[u]*2-1!=0)flag[v]=0;
		}
		dfs(v);
	}
	//能否以u為終點
	if(u==Root)flag[u]=0;
	else{
		if(frm[u])flag[u]=0;
		else if(flag[u]&&b[u].get_st(fa[u])==to[u]&&d_in[u]+d_out[u]+d_nu[u]*2-1!=0)flag[u]=0;
	}
}
void Init(){
	tot=0;
	for(int i=1;i<=n;++i){
		head[i]=0;
		d_nu[i]=d_in[i]=d_out[i]=frm[i]=to[i]=0;
	}
}
int main() {
	int Testcases=read();while(Testcases--){
		Init();
		n=read();
		for(int i=1;i<=n;++i)p[i]=read();
		if(n==1){
			puts("1");
			continue;
		}
		for(int i=1,u,v;i<n;++i){
			u=read(),v=read(),add_edge(u,v),add_edge(v,u);
			d_nu[u]++,d_nu[v]++;
			b[u].Init(v),b[v].Init(u);
			state[u][v]=state[v][u]=-1;
		}
		for(int i=1;i<=n;++i){
			Root=p[i];
			fa[Root]=0,flag[Root]=1;
			dfs(Root);
			int res=0;
			for(int j=1;j<=n;++j)if(flag[j]){res=j;break;}
			//assert(res!=0&&res!=Root);
			printf("%d ",res);
			int u=res;
			frm[u]=fa[u];
			while(fa[u]!=Root){
				//assert(state[u][fa[u]]==state[fa[u]][u]);
				if(state[u][fa[u]]==-1){
					state[u][fa[u]]=state[fa[u]][u]=fa[u];
					d_nu[u]--,d_nu[fa[u]]--;
					d_in[u]++,d_out[fa[u]]++;
				}else{
					//assert(state[u][fa[u]]==u);
					state[u][fa[u]]=state[fa[u]][u]=0;
					d_in[fa[u]]--,d_out[u]--;
				}
				int v=u;
				u=fa[u];
				b[u].union_s(fa[u],v);
			}
			to[Root]=u;
			if(state[u][fa[u]]==-1){
				state[u][fa[u]]=state[fa[u]][u]=fa[u];
				d_nu[u]--,d_nu[fa[u]]--;
				d_in[u]++,d_out[fa[u]]++;
			}else{
				//assert(state[u][fa[u]]==u);
				state[u][fa[u]]=state[fa[u]][u]=0;
				d_in[fa[u]]--,d_out[u]--;
			}
		}
		puts("");
	}
	return 0;
}

S D2T1 Emiya 家今天的飯

總結一下要求:

  • 總共要選至少一個位置(不能不選)。

  • 每行至多選一個位置。

  • 每列選的位置數量不超過所選總數的\(\frac{1}{2}\)

部分分:\(m=2\)\(3\)時,以\(m=3\)為例,我們可以設\(dp[i][a][b][c]\)表示考慮了\(i\)行,第\(1\)列選了\(a\)個,第\(2\)列選了\(b\)個,第\(3\)列選了\(c\)個的方案數。統計答案時要求\(a,b,c\leq\lfloor\frac{a+b+c}{2}\rfloor\)即可。復雜度\(O(n^{m+1})\)我考場上就寫的這個,得到了64分的好成績。

發現每列不超過\(\frac{1}{2}\)這個限制很特殊。如果我們枚舉哪些列不合法,那么容易發現,不合法的列至多只有\(1\)列。所以我們枚舉那一個不合法的列,然后求出:在強制這一列不合法的前提下的方案數。

\(dp[i][j][k]\)表示考慮了前\(i\)行,共選了\(j\)個數,強制不合法的列選了\(k\)個數,時的方案數。復雜度\(O(n^3m)\),可以得到\(84\)分的高分(Orz pmt!)。

考慮優化,其實我們關系的只是\(j\)\(k\)之間的關系。具體地,最后我們要求\(k>\lfloor\frac{j}{2}\rfloor\),即\(2k-j>0\)。所以我們可以把\([j][k]\)兩維壓成一維:令\(dp[i][x]\)表示考慮了前\(i\)行,\(2k-j=x\)時的方案數即可。由於\(x\)可能\(<0\),所以在具體實現時我們把第二維的值統一加\(n\)

時間復雜度\(O(n^2m)\)

參考代碼:

//problem:P5664
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fst first
#define scd second

typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;

inline int read(){
	int f=1,x=0;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
inline ll readll(){
	ll f=1,x=0;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}

const int MOD=998244353;
inline int mod(int x){return x<MOD?(x<0?x+MOD:x):x-MOD;}
int n,m,a[105][2005],s[105],dp[105][205];
int main() {
	n=read();m=read();int ans=1;
	for(int i=1;i<=n;++i){for(int j=1;j<=m;++j)a[i][j]=read(),s[i]=mod(s[i]+a[i][j]);ans=(ll)ans*(s[i]+1)%MOD;}
	ans=mod(ans-1);
	for(int l=1;l<=m;++l){
		memset(dp,0,sizeof(dp));
		dp[0][n]=1;
		for(int i=0;i<n;++i){
			for(int j=n-i;j<=n+i;++j){
				if(!dp[i][j])continue;
				dp[i+1][j+1]=mod(dp[i+1][j+1]+(ll)dp[i][j]*a[i+1][l]%MOD);
				dp[i+1][j-1]=mod(dp[i+1][j-1]+(ll)dp[i][j]*mod(s[i+1]-a[i+1][l])%MOD);
				dp[i+1][j]=mod(dp[i+1][j]+dp[i][j]);
			}
		}
		for(int j=n+1;j<=n+n;++j)ans=mod(ans-dp[n][j]);
	}
	cout<<ans<<endl;
	return 0;
}

S D2T2 划分

部分分:設\(dp[i][j]\)表示考慮了前\(i\)個數,當前段的結束位置為\(i\),上一段的結束位置為\(j\)時的最小代價。轉移時枚舉上上段的結束位置\(k\),從\(dp[j][k]\)轉移。對於每個\(i,j\),合法的\(k\)一定是一段后綴(位置可以二分出來),所以記錄\(dp\)第二維的后綴最小值即可轉移,復雜度\(O(n^2\log n)\)

我們為什么要用dp的第二維記錄一個\(j\)呢?我們把一個東西記錄在dp的一維里,必然是因為這個東西“有得有失”。比如做背包時,加入一個物品,增加了價值,但同時也增加了重量。我們不知道如何去平衡這個得失,或者說不知道在此時最優的情況所帶來的的“失”是否會使全局更劣,於是我們單開一維,把每一種情況都記錄下來,從而保證不錯過全局最優解。

回到本題,我們之所以不設\(dp[i]\),是因為這樣我們不知道上一段的和是什么,缺少條件,不好轉移。但是如果用一個數組記錄下每一個\(i\)是從哪里轉移過來的呢?這樣我們能否保證,\(j\)最有利的轉移點,也同時最有利於\(j\)后面所有需要從\(j\)轉移的點?也就是說,如果這個問題的答案是肯定的,那么不存在“有的有失”的情況,當前最優即全局最優,我們直接貪心、直接欽定,不用經過第二維DP的權衡。

考慮每次轉移時,選擇滿足條件的(上一段的和\(\leq\)當前段和的)點中最靠后的轉移。

可以證明,這樣做一定是最優的。

備注:我在網上找到了兩篇證明,但是證明並不優美且沒啥擴展價值,所以就不展開了。附兩個鏈接供讀者參考。

證明from cjy

證明from myy

感性理解一下結論好像又很顯然(霧)

在此結論的基礎上,每個位置\(i\)的上一段的結束位置就是確定的了,我們記為\(pre(i)\)。對\(a\)做前綴和,我們記\(s_i=\sum_{j=1}^{i}a_j\)

則最優的轉移點\(pre(i)\)一定是所有滿足\(s_i-s_j\geq s_j-s_{pre(j)}\)\(j\)中最大的。把這個條件轉化一下就是\(2s_j-s_{pre(j)}\leq s_i\)

我們維護一個單調隊列。在這個隊列中,從隊首到隊尾,元素的下標單調遞增,隊內元素\(j\)\(2s_j-s_{pre(j)}\)也單調遞增。因為如果存在一個\(k<j\)使得\(2s_k-s_{pre(k)}\geq2s_j-s_{pre(j)}\),則\(k\)在任何時候都不會有用。

當需要從\(i\)轉移時,我們檢查隊首。如果隊首的下一個元素滿足轉移條件,則可以把隊首彈掉。因為我們總是取滿足條件的元素中下標最大的。而\(s_i\)是單調遞增的,這個隊首如果現在沒有用,以后就再也不會有用了。

復雜度\(O(n)\)

為了避免空間爆炸,我們只開一個長度為\(n\)的數組,即前綴和數組。其他的信息都用結構體或pair裝在單調隊列里,這樣使用的內存大小為任意時刻的最大隊列長度,顯然是\(\leq n\)的。

參考代碼:

//problem:P5665
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fst first
#define scd second

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;

namespace Fread{
	const int MAXN=1<<20;
	char buffer[MAXN],*S,*T;
	inline char getchar(){
		if(S==T){
			T=(S=buffer)+fread(buffer,1,MAXN,stdin);
			if(S==T) return EOF;
		}
		return *S++;
	}
}
#ifdef ONLINE_JUDGE
	#define getchar Fread::getchar
#endif
inline int read(){
	int f=1,x=0;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
inline ll readll(){
	ll f=1,x=0;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}

const int MAXN=4e7+5;
int n;
ll s[MAXN];
__int128 dp[MAXN];
void big_input(){
	int x,y,z,b[2],pj,lj,rj;
	x=read(),y=read(),z=read(),b[1]=read(),b[0]=read(),read();
	const int moood=(1<<30);
	pj=0;
	for(int i=1,j=0;i<=n;++i){
		if(i>pj){
			++j;
			pj=read();lj=read();rj=read();
		}
		if(i>2)b[i&1]=((ll)b[(i&1)^1]*x+(ll)b[i&1]*y+z)%moood;
		s[i]=s[i-1]+(b[i&1]%(rj-lj+1))+lj;
	}
}
void print(__int128 ans){
	const ll base=1e18;
	if(ans<base)cout<<(ull)ans<<endl;
	else{
		cout<<(ll)(ans/base);
		ll t=ans%base;int len=0;
		while(t)len++,t/=10;
		for(int i=1;i<=18-len;++i)cout<<"0";
		if(ans%base)cout<<(ll)(ans%base)<<endl;
	}
}
int main() {
	n=read();int ty=read();
	if(!ty)for(int i=1;i<=n;++i)s[i]=s[i-1]+read();
	else big_input();
	dp[0]=0;
	deque<pli>dq;dq.push_back(mk(0,0));
	for(int i=1;i<=n;++i){
		while(dq.size()>1&&s[i]>=dq[1].fst)dq.pop_front();
		__int128 d=s[i]-s[dq.front().scd];dp[i]=dp[dq.front().scd]+d*d;
		while(!dq.empty()&&dq.back().fst>=s[i]+d)dq.pop_back();
		dq.push_back(mk(s[i]+d,i));
	}
	print(dp[n]);
	return 0;
}

S D2T3 樹的重心

不妨先令\(1\)為根。考慮以根作為重心的貢獻。

設根最大的子樹大小為\(mx\)

  • 如果割掉的邊不在最大的子樹內,則割去的子樹大小\(t\)要滿足\(mx\times 2\leq n-t\),即\(t\leq n-mx\times 2\)

  • 否則,割掉的邊在最大的子樹內,設次大子樹大小為\(smx\),則割去子樹大小\(t\)要滿足:\(smx\times 2\leq n-t,(mx-t)\times 2\leq n-t\),即\(mx\times2-n\leq t\leq n-smx\times2\)

我們用線段樹合並求出每個節點子樹內每種大小的子樹分別有多少。枚舉根的每個兒子,分別在根的線段樹上做區間查詢,就得到了根作為重心的出現次數。

如果暴力枚舉根,每次重新做線段樹合並,則復雜度\(O(n^2\log n)\)

考慮換根。發現如果以\(u\)為根,則相比於以\(fa(u)\)為根,在線段樹上的變化:只會增加一個\(n-size_u\),減少一個\(size_u\)。在dfs時用一個樹狀數組維護根到當前節點的路徑上,哪些\(size\)值被增加了,哪些\(size\)值被減少了,回溯時在樹狀數組上消除貢獻即可。

需要注意的是,線段樹合並后,原線段樹上的信息就會被覆蓋,所以我們不能在換根時再到\(u\)的線段樹上進行查詢,而要在第一遍dfs到\(u\)時就把換根需要的查詢信息預處理好。

參考代碼:

//problem:P5666
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fst first
#define scd second

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;

namespace Fread{
	const int MAXN=1<<20;
	char buffer[MAXN],*S,*T;
	inline char getchar(){
		if(S==T){
			T=(S=buffer)+fread(buffer,1,MAXN,stdin);
			if(S==T)return EOF;
		}
		return *S++;
	}
}
#ifdef ONLINE_JUDGE
	#define getchar Fread::getchar
#endif
inline int read(){
	int t;cin>>t;return t;
	int f=1,x=0;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
inline ll readll(){
	ll f=1,x=0;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}

const int MAXN=3e5;
struct EDGE{int nxt,to;}edge[MAXN<<1];
int n,head[MAXN],tot;
inline void add_edge(int u,int v){edge[++tot].nxt=head[u],edge[tot].to=v,head[u]=tot;}
int rt[MAXN];
struct SegmentTree{
	int cnt,ls[MAXN*40],rs[MAXN*40],sum[MAXN*40];
	void modify(int &p,int l,int r,int pos,int x){
		if(!p)p=++cnt;assert(cnt<MAXN*40);
		sum[p]+=x;
		if(l==r)return;
		int mid=(l+r)>>1;
		if(pos<=mid)modify(ls[p],l,mid,pos,x);
		else modify(rs[p],mid+1,r,pos,x);
	}
	int merge(int x,int y){
		if(!x||!y)return x+y;
		int z=++cnt;assert(cnt<MAXN*40);
		sum[z]=sum[x]+sum[y];
		ls[z]=merge(ls[x],ls[y]);
		rs[z]=merge(rs[x],rs[y]);
		return z;
	}
	int query(int p,int l,int r,int ql,int qr){
		if(ql>qr||!p)return 0;
		if(ql<=l&&qr>=r)return sum[p];
		int mid=(l+r)>>1,res=0;
		if(ql<=mid)res=query(ls[p],l,mid,ql,qr);
		if(qr>mid)res+=query(rs[p],mid+1,r,ql,qr);
		return res;
	}
	void Init(){
		for(int i=1;i<=cnt;++i)ls[i]=rs[i]=sum[i]=0;cnt=0;
	}
	SegmentTree(){}
}T1;
int sz[MAXN],f[MAXN],L[MAXN],R[MAXN];
void dfs1(int u,int fa){
	sz[u]=1;
	for(int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==fa)continue;
		dfs1(v,u);
		sz[u]+=sz[v];
	}
	int mx=n-sz[u],smx=0;
	for(int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==fa)continue;
		if(sz[v]>mx)smx=mx,mx=sz[v];
		else if(sz[v]>smx)smx=sz[v];
	}
	for(int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==fa)continue;
		if(smx==mx||sz[v]!=mx){
			f[u]+=T1.query(rt[v],1,n,1,n-2*mx);
		}else{
			f[u]+=T1.query(rt[v],1,n,2*mx-n,n-2*smx);
		}
	}
	for(int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==fa)continue;
		rt[u]=T1.merge(rt[u],rt[v]);
	}
	if(fa){
		if(smx==mx||n-sz[u]!=mx){
			f[u]-=T1.query(rt[u],1,n,L[u]=1,R[u]=n-2*mx);
		}else{
			f[u]-=T1.query(rt[u],1,n,L[u]=2*mx-n,R[u]=n-2*smx);
		}
		T1.modify(rt[u],1,n,sz[u],1);
	}
}
ll ans;
struct BIT{
	int c[MAXN];
	void modify(int p,int x){
		for(;p<=n;p+=(p&(-p)))c[p]+=x;
	}
	int query(int p){
		int res=0;
		for(;p;p-=(p&(-p)))res+=c[p];
		return res;
	}
	int query(int l,int r){
		l=max(l,1),r=min(r,n);
		if(l>r)return 0;
		return query(r)-query(l-1);
	}
	void Init(){
		memset(c,0,sizeof(int)*(n+3));
	}
	BIT(){}
}T2;
void dfs2(int u,int fa){
	if(fa){
		T2.modify(sz[u],-1);
		T2.modify(n-sz[u],1);
		f[u]+=T1.query(rt[1],1,n,L[u],R[u])+T2.query(L[u],R[u]);
	}
	//cout<<"* "<<u<<" "<<f[u]<<endl;
	ans+=(ll)u*f[u];
	for(int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==fa)continue;
		dfs2(v,u);
	}
	if(fa){
		T2.modify(sz[u],1);
		T2.modify(n-sz[u],-1);
	}
}
void Init(){
	memset(head,0,sizeof(int)*(n+3));tot=0;
	memset(rt,0,sizeof(int)*(n+3));T1.Init();
	T2.Init();
	memset(f,0,sizeof(int)*(n+3));
}
int main() {
	//freopen("data.txt","r",stdin);
	int Testcases=read();while(Testcases--){
		Init();
		n=read();
		for(int i=1,u,v;i<n;++i)u=read(),v=read(),add_edge(u,v),add_edge(v,u);
		//for(int i=1;i<=n;++i)ans+=(ll)i*solve_root(i);
		dfs1(1,0);
		ans=0;dfs2(1,0);
		printf("%lld\n",ans);
	}
	return 0;
}

J T2 公交換乘

\(P=\max_{i=1}^{n}\{price_i\}\)我敲了一個O(nP)的模擬就直接AC了(大霧)

\(O(nP)\)代碼片段:

const int MAXN=1e5+5;
int n;
struct node{int type,price,t;}a[MAXN];
deque<int>dq[1005];
int main() {
	n=read();
	for(int i=1;i<=n;++i)a[i].type=read(),a[i].price=read(),a[i].t=read();
	int ans=0;
	for(int i=1;i<=n;++i){
		if(a[i].type==0){
			ans+=a[i].price;
			dq[a[i].price].push_back(i);
		}
		else{
			int opt=-1;
			for(int j=a[i].price;j<=1000;++j){
				while(!dq[j].empty()&&a[dq[j].front()].t<a[i].t-45)dq[j].pop_front();
				if(!dq[j].empty()){
					if(opt==-1||a[dq[j].front()].t<a[dq[opt].front()].t)opt=j;
				}
			}
			if(opt==-1)ans+=a[i].price;
			else dq[opt].pop_front();
		}
	}
	cout<<ans<<endl;
	return 0;
}

這里講一下\(O(n\log P)\)的做法。

我們開一棵下標為票價的值域的線段樹。線段樹的每個葉子節點上放一個deque。非葉子節點存子樹內每個dequefront(首元素)的最小值(deque里存的值是車票的編號,其最小值也就是出現時間最早的車票)。特別地,如果一個deque為空,則令該葉子的值為無窮大。

對於一輛地鐵,我們直接在它對應的票價的deque上新加入(push_back)一個下標。

對於一輛公交,我們要查詢所有尚未使用的、票價不超過\(price_i\)的車票里時間最早的。相當於在線段樹上查詢一段后綴的最小值

  • 如果最小值\(<t_i-45\),則直接將其彈出,因為它已經過期,以后也不會再有用了。繼續查詢。

  • 如果查詢到的最小值為無窮大,說明所有合法的票價都為空了,我們沒有匹配到合法的地鐵,這輛公交車必須自己買票。

  • 否則說明查到了一班合法的地鐵。將其彈出(表示已使用)。

因為每輛地鐵只會入隊列、出隊列一次,每次加入、彈出時涉及到的線段樹操作是\(O(\log P)\)的。所以總復雜度\(O(n\log P)\)

參考代碼:

//problem:P5661
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fst first
#define scd second

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;

namespace Fread{
	const int MAXN=1<<20;
	char buffer[MAXN],*S,*T;
	inline char getchar(){
		if(S==T){
			T=(S=buffer)+fread(buffer,1,MAXN,stdin);
			if(S==T)return EOF;
		}
		return *S++;
	}
}
#ifdef ONLINE_JUDGE
	#define getchar Fread::getchar
#endif
inline int read(){
	int f=1,x=0;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
inline ll readll(){
	ll f=1,x=0;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}

const int MAXN=1e5+5,MAXP=1005,INF=1e9;
int n;
struct node{int type,price,t;}a[MAXN];
struct SegmentTree{
	int val[MAXP],pos[MAXN<<2];
	deque<int>dq[MAXP];
	void push_up(int p){
		pos[p]=(val[pos[p<<1]]<=val[pos[p<<1|1]]?pos[p<<1]:pos[p<<1|1]);
	}
	void build(int p,int l,int r){
		if(l==r){
			val[l]=INF;
			pos[p]=l;
			return;
		}
		int mid=(l+r)>>1;
		build(p<<1,l,mid);
		build(p<<1|1,mid+1,r);
		push_up(p);
	}
	void modify(int p,int l,int r,int pos,int x){
		if(l==r){
			if(x==-1){
				assert(!dq[l].empty());
				dq[l].pop_front();
				val[l]=(dq[l].empty()?INF:dq[l].front());
			}
			else{
				dq[l].push_back(x);
				val[l]=dq[l].front();
			}
			return;
		}
		int mid=(l+r)>>1;
		if(pos<=mid)modify(p<<1,l,mid,pos,x);
		else modify(p<<1|1,mid+1,r,pos,x);
		push_up(p);
	}
	int query(int p,int l,int r,int ql,int qr){
		if(ql<=l&&qr>=r)return pos[p];
		int mid=(l+r)>>1,res=0;
		if(ql<=mid)res=query(p<<1,l,mid,ql,qr);
		if(qr>mid){
			int x=query(p<<1|1,mid+1,r,ql,qr);
			if(!res||val[res]>val[x])res=x;
		}
		assert(res!=0);
		return res;
	}
}T;
int main() {
	n=read();
	for(int i=1;i<=n;++i)a[i].type=read(),a[i].price=read(),a[i].t=read();
	T.build(1,1,1000);
	int ans=0;
	for(int i=1;i<=n;++i){
		if(a[i].type==0){
			ans+=a[i].price;
			T.modify(1,1,1000,a[i].price,i);
		}
		else{
			while(true){
				int x=T.query(1,1,1000,a[i].price,1000);
				if(T.val[x]==INF){
					ans+=a[i].price;
					break;
				}
				else if(a[T.val[x]].t<a[i].t-45){
					T.modify(1,1,1000,x,-1);
				}
				else{
					T.modify(1,1,1000,x,-1);
					break;
				}
			}
		}
	}
	cout<<ans<<endl;
	return 0;
}

J T3 紀念品

由於同一天內是可以既賣出又買入的,如果我們要將一樣紀念品連續持有好幾天,則我們可以視作在中間的每一天既賣出、又買入,這樣我們可以在每天剛開始的時候,把手上所有的紀念品都換成錢,如果要繼續持有,就在今天晚上再換回來。這樣的好處是我們只需要DP出當天把所有紀念品換成錢后手上最多有多少錢即可,不用考慮其它資產。

\(f(i)\)表示第\(i\)天,把所有紀念品換成錢后,手上最多有多少錢。用這些錢我們可以在當天買紀念品、在下一天賣掉,從而在下一天得到更多的錢。

在每一天內,我們可能會買很多種不同的紀念品。設\(g(x)\)表示在當前天,用\(x\)元錢,最多能在第二天(通過把手上的紀念品賣掉)換得多少錢。這相當於是完全背包的模型,因為每樣紀念品可以在錢數足夠的情況下買無限多個。每樣紀念品都是背包的物品,它們的重量是它們在當前天的價格,而它們的價值是它們在下一天的價格

對第\(i\)天,求出\(g\)后,則\(f(i+1)=g(f(i))\)

邊界是\(f(1)=m\)

復雜度\(O(TVn)\),其中\(V\)是任意時刻的最大金幣數量。

參考代碼:

//problem:P5662
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fst first
#define scd second

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;

namespace Fread{
	const int MAXN=1<<20;
	char buffer[MAXN],*S,*T;
	inline char getchar(){
		if(S==T){
			T=(S=buffer)+fread(buffer,1,MAXN,stdin);
			if(S==T)return EOF;
		}
		return *S++;
	}
}
#ifdef ONLINE_JUDGE
	#define getchar Fread::getchar
#endif
inline int read(){
	int f=1,x=0;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
inline ll readll(){
	ll f=1,x=0;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}

int t,n,m,a[105][105],f[105],g[10004];
int main() {
	t=read(),n=read(),m=read();
	for(int i=1;i<=t;++i)for(int j=1;j<=n;++j)a[i][j]=read();
	f[1]=m;
	for(int i=1;i<t;++i){
		g[0]=0;
		for(int j=1;j<=10000;++j){
			g[j]=max(j,g[j-1]);
			for(int k=1;k<=n;++k)if(j>=a[i][k])g[j]=max(g[j],g[j-a[i][k]]+a[i+1][k]);
		}
		f[i+1]=g[f[i]];
	}
	cout<<f[t]<<endl;
	return 0;
}

J T4 加工零件

發現編號為\(a\)的工人生產第\(L\)階段的零件,需要\(1\)提供原材料,當且僅當存在一條\(1\)\(a\)的路徑長度為\(L\)。因為這個“路徑”是允許在同一條邊上來回橫跳的,每橫跳一次路徑長度加\(2\),所以我們可以把條件改為:當且僅當存在一條\(1\)\(a\)的路徑,長度\(\leq L\)且奇偶性和\(L\)相同。

所以我們從\(1\)出發,對每個點求從\(1\)到它的奇數最短路和偶數最短路即可。因為每個點只會被入隊、出隊各一次,所以復雜度\(O(n)\)

參考代碼:

//problem:P5663
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fst first
#define scd second

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;

namespace Fread{
	const int MAXN=1<<20;
	char buffer[MAXN],*S,*T;
	inline char getchar(){
		if(S==T){
			T=(S=buffer)+fread(buffer,1,MAXN,stdin);
			if(S==T)return EOF;
		}
		return *S++;
	}
}
#ifdef ONLINE_JUDGE
	#define getchar Fread::getchar
#endif
inline int read(){
	int f=1,x=0;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
inline ll readll(){
	ll f=1,x=0;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}

const int MAXN=1e5+5,INF=0x3f3f3f3f;
struct EDGE{int nxt,to;}edge[MAXN<<1];
int n,m,q,head[MAXN],tot;
inline void add_edge(int u,int v){edge[++tot].nxt=head[u],edge[tot].to=v,head[u]=tot;}
int dis[MAXN][2];
void dij(){
	memset(dis,0x3f,sizeof(dis));
	dis[1][0]=0;
	queue<pii>q;q.push(mk(1,0));
	while(!q.empty()){
		int u=q.front().fst,t=q.front().scd;q.pop();
		for(int i=head[u];i;i=edge[i].nxt){
			int v=edge[i].to;
			int nd=t+1;
			if(nd<dis[v][nd&1]){
				dis[v][nd&1]=nd;
				q.push(mk(v,nd));
			}
		}
	}
}
int main() {
	n=read();m=read();q=read();
	for(int i=1,u,v;i<=m;++i)u=read(),v=read(),add_edge(u,v),add_edge(v,u);
	dij();
	while(q--){
		int u=read(),t=read();
		if(dis[u][t&1]<=t)puts("Yes");
		else puts("No");
	}
	return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM