1. 程式人生 > 其它 >stm32串列埠資料中斷接收(DMA、IDLE中斷)

stm32串列埠資料中斷接收(DMA、IDLE中斷)

線段樹

板子

線段樹可以在優秀(一般是 \(O(m\log n)\) )的時間複雜度內處理區間問題

其思想就是分治,如果一個區間可以直接操作,那麼直接操作就好了

否則就把一個區間 \([l,r]\) 均等地分為2個等大的子區間 \([l,mid],[mid+1,r]\) 遞迴處理

至多就有 \(O(\log n)\) 種大小的區間,容易證明每種區間數量不超過2個,於是複雜度是嚴格 \(O(\log n)\)

從上面的推導不難看出線段樹使用的前提是操作的區間之間相互不影響(比如關於數對的詢問一般就不行,因為兩個區間數對相互產生貢獻),對一個區間操作優於逐個點的操作,操作的區間是整數(浮點數的話可能精度會卡層數,不過並不是絕對不能用)

首先看一下線段樹的板子

template<typename node,typename act,int N=10005>
class segment_tree
{
	private:
		void (*pushdown)(node&,node&,node&);//下傳標記,記得清空父節點的資訊 
		node (*pushup)(node,node);//合併子節點,記得判斷有沒有兒子 
		bool (*change)(node&,act&);//修改節點資訊
		//返回是否成功,不成功則遞迴到左右兒子,比如區間取max 
		class seg_node
		{
			public:
				int lc,rc;
				node d;
				seg_node()=default;
		};
		seg_node s[N];
		//如果沒有不造成影響的節點,pushup時應判斷是否為虛節點 
		int rt,cnt;//根節點,節點數 
		int lw,up;//區間的下界、上界 
#undef assert
#define assert(_expr) for (; !(_expr);                                  \
    __builtin_exit(114514)  )

#define mid ((l+r)>>1)
//區間中點 
#define Lc s[id].lc
#define Rc s[id].rc
//左右兒子編號 
#define lson Lc,l,mid
#define rson Rc,mid+1,r
//左右兒子及其區間 
#define no_cross ( (!id) || (l>qr) || (r<ql)  )
//是否和目標區間無交 
#define in_range ( (l>=ql) && (r<=qr) )
//是否在目標區間中
#define data(x) s[x].d
#define now data(id)
//節點的資訊 
		inline void update(int id)
		{
			if(Lc) now=pushup(data(Lc),data(Rc));
		}
		inline void pushDown(int id)
		{
			if(!id) return;
			pushdown(now,data(Lc),data(Rc));//下傳標記
		}
		template<typename from_type>
		inline void build(from_type h,int &id,int l,int r)
		{
			if(!id) id=++cnt;//動態開點 
			if(l==r)
				now=node(*(h+l),l,r);//存在某種轉換關係,必須寫好 
				//可能需要用到區間長度,比如區間和 
			else
				build(h,lson),
				build(h,rson),
				update(id);//合併到父節點上 
		}
		inline void modify(int id,int l,int r,int ql,int qr,act &a)//考慮到a可能叫為複雜,用引用減少空間 
		{
			if(no_cross) return;
			if(in_range)
				if(change(now,a))
					return;//操作成功
			pushDown(id);//下傳標記 
			if(l!=r)
			{
				if(!Lc) Lc=++cnt,data(Lc)=node(l,mid);
				if(!Rc) Rc=++cnt,data(Rc)=node(mid+1,r);
			}//動態開點 
			modify(lson,ql,qr,a),
			modify(rson,ql,qr,a);//遞迴到左右子樹
			update(id);//更新父節點 
		}
		inline node query(int id,int l,int r,int ql,int qr)
		{
			if(no_cross) return doomy;
			if(in_range) return now;
			pushDown(id);//下傳標記
			return pushup( query(lson,ql,qr) , query(rson,ql,qr) );//遞迴到左右子樹 
		}
		
	public:
		node doomy;//作為虛節點
		segment_tree(node (*my_pushup)(node,node),void (*my_pushdown)(node&,node&,node&),bool (*my_change)(node&,act&))
		{
			pushup=(my_pushup),
			pushdown=(my_pushdown),
			change=(my_change),
			lw=1,up=0;
		}
		template<typename from_type>
		inline void build(from_type h,int l,int r)
		{
			lw=l,up=r;
			build(h,rt,l,r);
		}//基於一個序列建樹 
		inline void modify(int l,int r,act a)
		{
			assert(lw<=up){cerr<<"Not Build!\n";}//可以不用 
			modify(rt,lw,up,l,r,a);
		} //修改區間 
		inline node query(int l,int r)
		{
			assert(lw<=up){cerr<<"Not Build!\n";}
			return query(rt,lw,up,l,r);
		} //區間詢問 
		inline void vir_build(int l,int r)
		{
			lw=l,up=r;
			rt=cnt=1;
		}//只設定區間不建樹,之後動態開點 
#undef mid
#undef Lc
#undef Rc
#undef lson
#undef rson
#undef no_cross
#undef in_range
#undef data
#undef now
#undef assert
};//合併方式,下傳方式,修改規則

雖然操作已經在註釋中寫得比較清楚了,但不結合例子依然比較難懂,接下來就是一些例子

運用

a simple example

區間加,區間求和

考慮維護一個加法標記,表示整個區間被加了多少,然後一個點的值就是所有線上段樹的劃分下包含它的區間1的加法標記的和

對於區間求和,只需要在維護加法標記的同時乘上區間長度,就可以維護區間和了

於是可以寫出如下程式碼

inline bool add(node& a,act& x)//act是操作類,只有一個int,node是樹的節點類,有pls和sum
{
	a.sum+=x.k*a.len;//維護區間和
	a.pls+=x.k;//維護加法標記
	return true;
}

返回值在此處沒有意義,但對於部分線段樹有意義

接著考慮詢問,一個小細節是如果大的區間包含詢問區間,那麼需要統計其標記,一種方法是記錄沿途的標記的等效,也就是標記永久化的寫法,這裡我採用了另一種方法,就是直接下放標記,兩種方式沒有明顯差異(當然如果你需要可持久化那就必須標記永久化了)

程式碼如下

inline void pd(node a,node &b)
{
	b.sum+=a.pls*b.len;//維護區間和
	b.pls+=a.pls;//顯然加法標記直接疊加
}
inline void pdd(node& a,node& b,node& c)
{
	pd(a,b),pd(a,c);//下傳標記
	a.pls=0;//記得要刪除父節點的標記
}

線段樹的劃分方法提供了一種把任意區間劃分為 \(O(\log n)\) 個不交的子區間的方法,顯而易見,我們還需要把這些區間的資訊合併起來

inline node merge(node a,node b)
{
	node tmp;
	tmp.sum=a.sum+b.sum;
	tmp.len=a.len+b.len;
	return tmp;
}

看看 \(node\)

class node
{
	public:
		ll sum;
		ll pls;
		int len;
		node()=default;
		node(int l,int r)
		{len=r-l+1;}
		node(ll x,int l,int r)
		{sum=x,len=r-l+1;}
};

為什麼要有3個建構函式有一點難以理解

實際上是因為動態開點是隻知道所在區間,所以需要第二個建構函式,而靜態建樹即知道所在區間,又知道區間資訊,就需要第三個建構函式

上文的是模板,就算維護的資訊和所在區間無關,也必須有這三類建構函式

然後這樣宣告一顆線段樹

segment_tree<node,act,maxn*2> t(merge,pdd,add);

模板引數的意義依次是:節點資訊,操作資訊,樹的大小

建構函式的引數依次是:節點合併規則,標記下放規則,操作規則

\(modify\) 沒有返回值,進行操作,如果有多種操作,就只能寫在操作規則內了

\(query\) 得到一個區間的合併的節點資訊,返回一個節點類

\(vir\_build\) 設定區間的大小並只建立根節點

\(build\) 需要一個隨機訪問容器的首指標和一個區間,建立一顆線段樹

線段樹1AC程式碼