1. 程式人生 > 實用技巧 >2020.08.08 / 09 模擬賽題解

2020.08.08 / 09 模擬賽題解

08.07 的模擬賽因為版權原因沒有辦法公開。

Day 1 T1

題意

給定一棵二叉樹,非葉節點的度數為 \(0\)。求最小的操作次數,每次操作交換一個非葉節點的兩棵子樹,來使左邊的所有葉節點深度大於等於右邊的葉節點,且葉節點的深度最大值和最小值之差不大於 \(1\)

輸入格式

第一行一個正整數 \(n\),表示非葉節點的數量。

\(2\)\(n+1\) 行,第 \(i+1\) 行兩個整數,表示節點 \(i\) 的左右兩個兒子的編號。如果其兒子是葉節點,則編號為 \(-1\)

輸出格式

如果可能滿足題目的條件,輸出最小的操作次數,否則輸出 \(-1\)

對於 \(60\%\) 的資料,滿足 \(1≤n≤20\)


對於 \(100\%\) 的資料,滿足 \(1≤n≤10^5\)


\(60\)

觀察到操作順序不影響答案,\(O(2^n)\) 列舉每個節點是否交換子樹,然後 \(O(n)\) 檢查最終的樹是否符合要求。

複雜度 \(O(2^n \times n)\)

\(100\)

略複雜的分類討論。

首先判斷掉深度最大的葉節點和深度最小的葉節點深度差大於 \(1\) 的情況。因為交換子樹不會影響葉節點深度,所以這種情況一定無解。

對於每個節點 \(i\),記 \(v_i\)\(\text{maxdep}_i\)。其中,\(v_i=1\) 表示該節點為根的子樹中所有節點深度都相等,否則 \(v_i=0\)

\(\text{maxdep}_i\) 表示該節點為根的子樹中深度最大的葉節點。

考慮自底向下維護 \(v_i\),而 \(\text{maxdep}\) 在之前用一次 DFS 求出。對於每個非葉節點,設其左兒子為 \(L\),右兒子為 \(R\)。有幾種可能的情況:

  • \(v_L=v_R=1,\text{maxdep}_L = \text{maxdep}_R\)

    這種情況下,以 \(i\) 為根的子樹中,葉節點深度完全相等。不需要任何操作。這種情況下,\(v_i=1\)

對於以下情況,\(v_i\) 均為 \(0\)

  • \(v_L=v_R=1,\text{maxdep}_L \neq \text{maxdep}_R\)

    這種情況下,\(i\) 的左子樹和右子樹中,葉節點的深度完全相等,但 \(i\) 為根的子樹並不。根據題意,如果 \(\text{maxdep}_L < \text{maxdep}_R\),則需要一次操作。

  • \(v_L=0,v_R=1\)

    這種情況下,\(i\) 的右子樹中葉節點深度完全相等,但左子樹並不。如果 \(\text{maxdep}_L \leq \text{maxdep}_R\),那麼需要一次操作。

  • \(v_L=1,v_R=0\)

    這種情況下,\(i\) 的左子樹中葉節點深度完全相等,但右子樹並不。如果 \(\text{maxdep}_L < \text{maxdep}_R\),那麼需要一次操作。

  • \(v_L=v_R=1\)

    這種情況下,\(i\) 的左右子樹中葉節點深度均不完全相等,根據題意,這是無解的。

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

# include <bits/stdc++.h>
# define rr
const int N=500010,INF=0x3f3f3f3f;
int son[N][2];
int ans;
int m;
int n;
int maxdep[N],depth[N];
int allmax;
bool leaf[N],bad[N]; // bad 即為上文中的 v_i
inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-')f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}
void dfs(int i,int fa){
	depth[i]=maxdep[i]=depth[fa]+1;
	allmax=std::max(allmax,depth[i]);
	if(leaf[i])
		return;
	for(rr int j=0;j<=1;++j){
		int to=son[i][j];
		if(to==fa)
			continue;
		dfs(to,i);
		maxdep[i]=std::max(maxdep[i],maxdep[to]);
	}
	return;
}
void solve(int i,int fa){
	if(leaf[i])
		return;
	for(rr int j=0;j<=1;++j){
		solve(son[i][j],i);
	}
	if(bad[son[i][0]]&&bad[son[i][1]]){ // 完全相等
		printf("-1"),exit(0);
	}
	if(!bad[son[i][0]]&&!bad[son[i][1]]){ // 無解
		if(maxdep[son[i][0]]==maxdep[son[i][1]])
			return;
		else
			bad[i]=true,ans+=((maxdep[son[i][0]]<maxdep[son[i][1]]));
		return;
	}
	if(bad[son[i][0]]&&maxdep[son[i][0]]<=maxdep[son[i][1]])
		++ans;
	if(bad[son[i][1]]&&maxdep[son[i][1]]>maxdep[son[i][0]])
		++ans;
	bad[i]=true;	
	return;
}
int main(void){
//	freopen("mobiles.in","r",stdin);
//	freopen("mobiles.out","w",stdout);
	n=m=read();
	for(rr int i=1;i<=m;++i){
		int u=read(),v=read();
		if(u==-1)
			++n,u=n,leaf[u]=true;
		if(v==-1)
			++n,v=n,leaf[v]=true;
		son[i][0]=u,son[i][1]=v;	
	}
	dfs(1,0);
	for(rr int i=1;i<=n;++i){ // 深度差過大
		if(leaf[i]&&abs(depth[i]-allmax)>1){
			printf("-1"),exit(0);
		}
	}
	solve(1,0);
	printf("%d",ans);
	return 0;
}

Day 1 T2

題意

給定 \(n\)\(m\) 條邊的無向圖,點有點權 \(w_i\),邊 \((u,v)\) 有邊權 \(w_{u,v}\)

你可以選擇一些點。如果點 \(i\) 被選擇,則其對答案貢獻 \(w_i\)

對於邊 \((u,v)\)

  • \(u\)\(v\) 均被選擇,對答案貢獻 \(w_{u,v}\)
  • \(u\)\(v\) 選擇了一個,對答案貢獻 \(0\)
  • \(u\)\(v\) 均未被選擇,對答案貢獻 \(-w_{u,v}\)

求最大答案。

輸入格式

第一行一個正整數 \(n\)

接下來一行 \(n\) 個整數 \(w_1,w_2,...,w_n\)

接下來一行一個正整數 \(m\)

接下來 \(m\) 行,每行三個整數 \(u_i,v_i,w_{u_i,v_i}\),表示存在邊 \((u_i,v_i)\),邊權為 \(w_{u_i,v_i}\)

輸出格式

一行一個整數,表示最大答案。

對於 \(40\%\) 的資料,\(m=0\)
對於另外 \(40\%\) 的資料,\(n,m \leq 20\)
對於 \(100\%\) 的資料,\(1 \leq n,m \leq 10^5,1 \leq u_i,v_i \leq n,|w_i| \leq 10^9\)


\(40\)

選取大於 \(0\)\(w_i\)

另外 \(40\)

\(O(2^n \times (n+m))\) 暴力列舉。

\(100\)

對於每條邊,將邊權加到兩個端點的點權上,則答案為

\[\text{ans} = -(\sum w_{u,v}) + \sum \limits_{i=1}^{n} \max\{w_i,0\} \]

即:所有大於 \(0\) 的點權之和減去邊權之和。

注意此時 \(w_i\) 是被修改後的。

感性理解一下正確性。如果一條邊兩個端點都被選擇,那麼這條邊權被多算了一次,需要減去。如果一條邊選擇了一個端點,那麼這條邊權不應該被加上,所以需要減掉。如果一條邊兩個端點都沒選,根據題意,貢獻為邊權的相反值,所以也需要減掉。

所以 \(-(\sum w_{u,v})\) 不可避,我們只能想怎樣讓取到的點權最大。顯然,取大於 \(0\) 的點權就行。

程式碼待補。

Day 1 T3

題意

在平面直角座標系上有 \(n\) 個矩形,第 \(i\) 個矩形的左下角是 \((sx_i,sy_i)\),右上角是 \((tx_i,ty_i)\),且滿足 \(sx_i,sy_i>0\)

現在有 \(q\) 次詢問,每次詢問左下角為 \((0,0)\),右上角為 \((t,t)\) 的矩形與給出的 \(n\) 個矩形相交的面積。

輸入格式

第一行兩個正整數 \(n,q\)

接下來 \(n\) 行,每行四個整數 \(sx_i,sy_i,tx_i,ty_i\)

接下來 \(q\) 行,每行一個整數 \(t_i\),表示一次詢問。

輸出格式

對於每個詢問,輸出該矩形與給出的 \(n\) 個矩形相交的面積,重複的部分算多次

對於 \(30\%\) 的資料,\(0 \leq t_i,sx_i,sy_i,tx_i,ty_i \leq 10^3\)
對於 \(50\%\) 的資料,\(n \leq 10^3,q \leq 10^3\)
另有 \(20\%\) 的資料,\(q=1\)
對於 \(100\%\) 的資料,\(n \leq 5 \times 10^4,q \leq 10^5 ,0\leq t_i,sx_i,sy_i,tx_i,ty_i \leq 5 \times 10^6,sx_i<tx_i,sy_i<ty_i\)


30 分

考慮對於每一個 \(1 \times 1\) 的小矩形,記錄它被多少個矩形覆蓋過。詢問時暴力統計。\(O((n+q) \times tx_i \times ty_i)\)

70 分

對於每次詢問,遍歷每一個矩形,用數學方法算出該詢問中的正方形與每個矩形相交的面積。\(O(nq)\)

100 分

考慮讓每個矩形轉換為四個左下角為 \((0,0)\) 的矩形的面積之和 / 差。

對於上圖的矩形 \(BCEF\),其面積為 \(ACIG-ABHG-DFIG+DEHG\)。我們可以給每個矩形加上符號,例如 \(+ACIG,-ABHG,-DFIG,+DEHG\),這樣可以在統計答案的時候知道該矩形對答案的貢獻是正還是負。

轉換後,每個矩形只需要記錄右上角座標 \((x,y)\) 即可,因為左下角座標均為 \((0,0)\)

對於一個右上角座標為 \((x,y)\) 且符號為正的矩形,它和詢問矩形的位置關係有四種情況:

  • \(t < x,y\)

顯然這種情況下詢問矩形被當前矩形覆蓋,貢獻為 \(t^2\)

  • \(y \leq t, x > t\)

這種情況下,貢獻為 \(y \times t\)

  • \(x \leq t,y > t\)

圖與上一種情況類似,貢獻為 \(x \times t\)

  • \(x,y \leq t\)

這種情況下詢問矩形覆蓋了當前矩形,貢獻為 \(xy\)

符號為負時,貢獻取反即可

第一種情況可以用一個變數 \(task_1\) 記錄這樣情況的矩形有多少個。根據乘法分配律,第二、三種情況可以用一個變數 \(task_2\) 記錄這樣情況的 \(x\)\(y\) 的總和。第三種情況可以直接記錄這種矩形的總面積。當然,如果符號為負,記錄時符號仍然需要取反

則一次詢問的答案為

\[\text{ans}=task_1\times t^2 + task_2 \times t+ task_3 \]

把一個矩形的 \(x,y\) 拆成兩個獨立的數,然後把 \(t_i\)\(x,y\) 放在一起排序。最初,所有的矩形都是第一種情況。在遇到 \(t_i\) 時,根據當前的 \(task_{1,2,3}\) 計算出答案。在遇到 \(x\)\(y\) 時,其對應的矩形情況發生了變化,將該矩形的貢獻從原來的情況中減去,並加入到新的情況中。

# include <bits/stdc++.h>
# define rr
# define int long long
const int N=500010,INF=0x3f3f3f3f;
struct Node{
	int val,id,v,ab;
	/*
	val 是橫座標 / 縱座標
	id 是矩陣編號 特殊地,如果當前為詢問則 id = INF + 詢問編號
	v 是矩陣符號,1 or -1
	ab = 0 為 x,ab = 1 為 y 
	*/ 
	bool operator < (const Node &rhs) const{ // 詢問放在最後處理 
		return val!=rhs.val?val<rhs.val:id<rhs.id;
	}
}t[N*2];
int tot,cnt;
int n,q;
int ans[N];
int nowx[N],nowy[N];
bool del[N];
inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-')f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}
inline void add(int x,int y,int v,int id){
	if(!x||!y){ // 矩形退化成線段或點的情況 沒必要處理 
		return;
	}
	t[++tot].val=x,t[tot].id=id,t[tot].v=v,t[tot].ab=0;
	t[++tot].val=y,t[tot].id=id,t[tot].v=v,t[tot].ab=1;
	return;
}
signed main(void){
	n=read(),q=read();
	for(rr int i=1;i<=n;++i){
		int sx=read(),sy=read(),tx=read(),ty=read();
		add(tx,ty,1,++cnt);
		add(sx,sy,1,++cnt);
		add(sx,ty,-1,++cnt);
		add(tx,sy,-1,++cnt); // 拆分 
	}
	for(rr int i=1;i<=q;++i){
		int x=read();
		t[++tot].val=x,t[tot].id=INF+i,t[tot].v=0; // 詢問 
	}
	std::sort(t+1,t+1+tot);//放在一起排序 
	int task_1=0,task_2=0,task_3=0;
	for(rr int i=1;i<=tot;++i){
		task_1+=t[i].v;
	}
	task_1/=2; // 每個矩形被算了兩次(x,y 各一次) 
	for(rr int i=1;i<=tot;++i){
		if(t[i].id>INF){ // 詢問 
			int trueid=t[i].id-INF,len=t[i].val; // 得到真實的詢問編號 
			ans[trueid]=task_1*(len*len)+task_2*len+task_3;
			continue;
		}
		if(t[i].ab==0){ // 記錄 x/y 已經出現 
			nowx[t[i].id]=t[i].val;
		}else{
			nowy[t[i].id]=t[i].val;
		}
		if(nowx[t[i].id]&&nowy[t[i].id]){ // 第二種 / 第三種 -> 第四種 
			task_2-=(t[i].ab?(nowx[t[i].id]):(nowy[t[i].id]))*t[i].v,task_3+=nowx[t[i].id]*nowy[t[i].id]*t[i].v;
		}else if(nowx[t[i].id]||nowy[t[i].id]){ // 第一種 -> 第二種 / 第三種 
			int suv=nowx[t[i].id]|nowy[t[i].id];
			task_1-=t[i].v,task_2+=suv*t[i].v;
		};
	}
	for(rr int i=1;i<=q;++i){
		printf("%lld\n",ans[i]);
	}
	return 0;
}