1. 程式人生 > >[JZOJ5229]【GDOI2018模擬7.14】小奇的糖果

[JZOJ5229]【GDOI2018模擬7.14】小奇的糖果

題目

在這裡插入圖片描述

題目大意

在一個二維的平面上,有一堆有顏色的點,你需要找出一條水平線段,使得這個線段上面(或者是下面)的點的顏色不包含所有的顏色。問點數最大是多少。


思考歷程

在一開始,我看錯了題目大意。
題目說的是線段,而我理解的是直線。
然後推了好多遍樣例,覺得樣例錯了。
後來才發現題目給的是線段。
不然這題就是一個大大的水題了

估計一下時間複雜度,嗯,應該是 O ( n

lg n ) O(n \lg n) O ( n lg
2 n ) O(n \lg^2 n)

往這個方面想了好久,想不出來。
於是退而求其次,想 O (
n 2 ) O(n^2)

這個時間複雜度還是很容易做的。
顯然地,我們肯定要先以縱座標排一遍序。
然後從上往下掃(後面還要從上往下掃)。
對於每個橫座標,記錄一下當前被掃過的點。
那麼,相當於是,在掃描線上,找到一個合法區間,使得這個區間最大。
我們可以先固定左端點,然後右端點儘量延伸。用一個桶就可以了,還比較簡單。
然後就愉快地拿到了60分了。


正解

宣告:現在我還沒有AC這題,先總結一下,理清思路。
正解是 O ( n lg n ) O(n \lg n) 的。
假設我們取的是掃描線上面的點(下面的道理是一樣的)。
我們先將掃描線移到最下方,很顯然,在這時候所有點都在掃描線的上面,這就是它們的終極狀態。
用一個樹狀陣列維護一段橫座標的區間中的點數。
在用一個雙向連結串列來維護一下在它左邊的離他最近的同顏色的點,右邊同理。
那麼,對於一個點,它的前驅和它之間沒有和它們同顏色的點,右邊同理。
我們先把終極狀態的答案求出來,即是列舉哪個顏色不選,然後在沒有這些顏色的區間中用樹狀陣列求出點數,試著更新答案。
接著我們考慮將掃描線向上移動。
在移動的時候,有一些點去到了掃描線的下面,那麼我們就要將它們在樹狀陣列中的貢獻減去,將它們從雙向連結串列中刪去。
一個點從雙向連結串列中刪去後,它之前的前驅和後繼之間沒有和它顏色相同的點,所以我們可以這個區間試著更新答案。
然後整一題就如此愉快地解決了。

其中有一個很尷尬的地方是,由於一條掃描線可能同時掃過多個點,然而在統計這些答案的時候可能會對互相有影響。有點腦抽,然後想了很久,最終驚奇的發現,其實……
我們可以先在樹狀陣列中減去它的貢獻,在掃描線換行的時候,我們就將它們從雙向連結串列中刪去,並統計答案。
由於我們已經在樹狀陣列中減去了它的貢獻,那麼在後面,如果一個點本應被刪除,但是還沒有刪除就在前面的點中多計算了它的貢獻,那麼這個貢獻是會被覆蓋的。


吐槽

最近總喜歡吐槽一下,什麼東西都吐槽一下。
打程式碼的時候細節可能比較多,有時沒有注意它在排序前後的對應位置,導致了我的程式調了好久,很不爽……
(我的程式中,先對橫座標進行了排序,預處理了雙向連結串列,然後再按縱座標排序。可是,在排序前後的序號我在一開始忘記要對上,於是……)
還有,為什麼明明是我講題,卻在一群人對了這題之後才對?


程式碼

using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cassert>
#define N 100000
int n,m,tot;
struct Candy{
	int x,y,c;
	int num; 
} d[N+10];
inline bool cmpx(const Candy &a,const Candy &b){
	return a.x<b.x;
}
inline bool cmpy(const Candy &a,const Candy &b){
	return a.y<b.y;
}
int ans;
int t[N+10];
#define lowbit(x) ((x)&(-(x)))
inline int add(int k,int x){
	for (;k<=m;k+=lowbit(k))
		t[k]+=x;
}
inline int query(int k){ 
	int res=0;
	for (;k;k-=lowbit(k))
		res+=t[k];
	return res;
}
int w[N+10];//w[i]表示編號i的點(以橫座標排序之後的順序)的編號
int l[N+10],r[N+10],tmpl[N+10],tmpr[N+10];//這些東西記錄的都是以橫座標排序後的編號
int last[N+10];//記錄某種顏色最後的點的編號
inline void init(){
	sort(d+1,d+n+1,cmpx);
	m=0;
	for (int i=1,lst=-2147483648;i<=n;++i){
		if (d[i].x!=lst)
			m++,lst=d[i].x;
		d[i].x=m;
	}
	//以上這段是離散化(如果你開心,不離散化打動態開點線段樹,我也沒意見)
	for (int i=1;i<=n;++i){
		d[i].num=i; 
		w[i]=d[i].x;
	}
	memset(last,0,sizeof last);
	for (int i=1;i<=n;++i){
		l[i]=last[d[i].c];
		r[l[i]]=i;
		last[d[i].c]=i;
	}
	for (int i=1;i<=tot;++i)
		r[last[i]]=n+1;
	memcpy(tmpl,l,sizeof l);
	memcpy(tmpr,r,sizeof r);
}
int main(){
	int T;
	scanf("%d",&T);
	while (T--){
		scanf("%d%d",&n,&tot);
		for (int i=1;i<=n;++i)
			scanf("%d%d%d",&d[i].x,&d[i].y,&d[i].c);
		init();
		for (int i=1;i<=n;++i)
			add(d[i].x,1);
		w[0]=0;
		w[n+1]=m+1;
		ans=0;
		for (int i=1;i<=tot;++i){
			if (w[last[i]]+1<=m+1)
				ans=max(ans,query(m)-query(w[last[i]]));
			for (int j=last[i];j;j=l[j])
				if (w[l[j]]+1<w[j])
					ans=max(ans,query(w[j]-1)-query(w[l[j]]));
		}
		//以上是統計全部的答案(就是掃描線在全部點上面或下面的情況)
		sort(d+1,d+n+1,cmpy);
		d[0].y=d[n+1].y=-2147483648;
		for (int i=1,j=1;i<=n;++i){
			add(d[i].x,-1);
			if (d[i].y!=d[i+1].y)
				for (;j<=n && d[j].y<=d[i].y;++j){
					int jj=d[j].num;
					r[l[jj]]=r[jj];
					l[r[jj]]=l[jj];
					if (w[l[jj]]+1<w[r[jj]])
						ans=max(ans,query(w[r[jj]]-1)-query(w[l[jj]]));
				}
		}
		memset(t,0,sizeof t);
		swap(l,tmpl),swap(r,tmpr);
		for (int i=1;i<=n;++i)
			add(d[i].x,1);
		for (int i=n,j=n;i>=1;--i){
			add(d[i].x,-1);
			if (d[i].y!=d[i-1].y)
				for (;j>=1 && d[j].y>=d[i].y;--j){
					int jj=d[j].num;
					r[l[jj]]=r[jj];
					l[r[jj]]=l[jj];
					if (w[l[jj]]+1<w[r[jj]])
						ans=max(ans,query(w[r[jj]]-1)-query(w[l[jj]]));
				}
		}
		printf("%d\n",ans);
	}
	return 0;
}

總結

有的時候連結串列是一個好東西。
比如說在處理有沒有相同顏色之類的問題的時候,用連結串列記錄一下左右最近的同顏色的點或許是一個很好的解題方向。
有的時候正著搞不容易,那就反著搞,全部加進去,然後刪掉。
還有在做平面問題時應當往掃描線上想一想。