單調隊列(尺取法) 學習筆記


尺取法

引子

說實話,這部分其實我也才學了3天,剛開始接觸時,是做了一個小小粉絲嘟嘟熊_hdu6119,聽T老師講的時候,感覺跟之前做的斜率優化,就是我之前寫的HNOI的玩具裝箱 ,差不多,都是用了一個單調隊列,來優化,其實重要的可以應用的原因是wyq所說的單調

我們來看看一個明顯的單調隊列的例子

Eg. 有這么一行數\(a_1,a_2,a_3,...,a_n\),我們要求所有任意連續k個數中的最小值。

我們平常的方法是什么,枚舉一個起點\(head\)循環到\(head+k-1\),求出其中的最小值 ,再把所有的最小值比較,即可得出答案,時間復雜度是\(O(kn)\)

用了單調法,\(O(kn)\)\(O(n)\)

主要分為以下幾步

  1. 初始化 \(head=1,tail=0,a_{0}=0x3f3f3f3f\),循環自變量為\(c\)
  2. 檢查\(que_{head}\) 是否\(>=tail-k+1\) 若是,繼續,否則,\(head++\),直到滿足條件
  3. \(a_{c}\)滿足小於等於\(a_{que_{tail}}\)\(head<=tail\),,\(tail--\)
  4. \(c\)入隊
  5. \(c>=k\)時,\(a_{que_{head}}\)即為所求

這是單調隊列的簡單應用,我們來進入正題

正文

用我的第一道題做第一道例題吧

小小粉絲嘟嘟熊_hdu6119

我們先不管算法,來談談如何合並一個區間

我的初步想法是這樣的,先把一個個區間按照左端點排序,左端點相同,按右端點從小至大,然后掃一遍就行了‘

然后尺取,做一個\(tail\)\(head\),然后,你既然想要最大的連續的,那么頭指針單調時,tail必定單調,所以我們能使用尺取法

接下來是常規的尺取,算區間的間隔,看小不小k

bool cmp(Node a,Node b){
	if(a.l==b.l) return a.r>b.r;
	return a.l<b.l;
}
void init(){
	for(int i=1;i<=n;i++){
		scanf("%d%d",&a[i].l,&a[i].r);
	}
	sort(a+1,a+1+n,cmp);
	cnt=1,b[1].l=a[1].l,b[1].r=a[1].r;//預處理區間
	for(int i=2;i<=n;i++){
		if(a[i].l<=b[cnt].r+1&&a[i].r>b[cnt].r) b[cnt].r=a[i].r;
		if(a[i].l>b[cnt].r){
			cnt++;
			b[cnt].l=a[i].l,b[cnt].r=a[i].r;
		}
	}
	memset(que,0,sizeof que);
	head=1;
	ans=b[1].r-b[1].l+1+k;
	kong=0;
	return ;
}
int main(){
	while(scanf("%d%d",&n,&k)!=EOF){
		init();
		for(int i=2;i<=cnt;i++){//尺取法
			while(head<=i&&kong+b[i].l-b[i].r-1>k){
				kong=kong-(b[head+1].l-1-b[head].r);
				head++;
			}
			kong=kong+(b[i].l-b[i-1].r-1);
			ans=max(ans,k-kong+b[i].r-b[head].l+1);
		}
		printf("%d\n",ans);
	}
	return 0;
}

Eg.2

hdu1937Finding Seats

數據范圍,能給你啟示

我們如果平常的,枚舉一個矩陣左上,右下的話,時\(O(n^6)\),會超時,考慮到我們的座位,再上界,下界,左界一定時隨着右界的增大,Seats 是不減函數,在做前綴和,就能把時間復雜度降為\(O(n^3)\)可以在規定時間內過掉

所以能用尺取法

具體思路

枚舉上界和下界,對其中用尺取法,注意都是閉區間。。。

示例如下

#include <iostream>
#include <cstdio>
#include <cstring>
const int Maxn=303;
int r,c,k,s[Maxn][Maxn],ans;
char ch;
using namespace std;
int read(){
    int x=0;
    char ch=getchar();
    while(ch<'0'||ch>'9') ch=getchar();
    while(ch<='9'&&ch>='0'){
        x=(x<<1)+(x<<3)+(ch-'0');
        ch=getchar();
    }
    return x;
}

int count(int y1,int y2,int x1,int x2){
    return s[y2][x2]-s[y2][x1-1]-s[y1-1][x2]+s[y1-1][x1-1];
}
int main(){
    //freopen("hdu1937.in","r",stdin);
    while(1){
        scanf("%d%d%d%c",&r,&c,&k,&ch);
        if(r==0&&c==0&&k==0)break;
        memset(s,0,sizeof s);
        for(int i=1;i<=r;i++){//制作前綴和
            for(int j=1;j<=c;j++){
                ch=getchar();
                if(ch=='X')    s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1];
                else s[i][j]=1+s[i-1][j]+s[i][j-1]-s[i-1][j-1];
            }
            ch=getchar();
        }
        ans=0x7fffffff;
        for(int sh=1;sh<=r;sh++){
            for(int x=sh;x<=r;x++){//枚舉上界下界
                int tail=0;//枚舉head
                for(int head=1;head<=c;head++){
                    while(tail<c&&count(sh,x,head,tail)<k){
                        tail++;//位數如果小tail++
                    }
                    if(tail==c&&count(sh,x,head,tail)<k) break;//這一內不行
                    ans=min(ans,(tail-head+1)*(x-sh+1));//統計答案
                }
            }
        }
        printf("%d\n",ans);
    }
}

Eg.3

Kirinriki

這個題,他告訴你我們的分數會加絕對值,不減

而且 他是一個軸對稱的算法,並且不能有交集

我們可以用尺取法來解決,枚舉每個對稱軸,並不是有奇偶性之分

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
using namespace std;
const int Maxn=100000;
int T,m;
char c[Maxn];


int main(){
    //freopen("hdu6103.in","r",stdin);
    scanf("%d",&T);
    while(T--){
        scanf("%d",&m);//讀入 
        scanf("%s",c+1);    
        int ans=-1,len=strlen(c+1);
        
            for(int i=2;i<len;i++){//從第二個到len-1 
                int tail=0,now=0,min1=min(i-1,len-i);//算到兩邊長 
                for(int head=1;head<=min1;head++){
                    while(tail<min1&&now+abs(c[i-tail-1]-c[i+tail+1])<=m){ 
                        now+=abs(c[i-tail-1]-c[i+tail+1]);
                        tail++;
                    }
                    ans=max(ans,tail-head+1);
                    now-=abs(c[i-head]-c[i+head]);
                }
            }
        
            for(int i=1;i<len;i++){
                int tail=0,now=0,min1=min(i,len-i);
                for(int head=1;head<=min1;head++){
                    while(tail<min1&&now+abs(c[i-tail]-c[i+tail+1])<=m){
                        now=now+abs(c[i-tail]-c[i+tail+1]);
                        tail++;
                    }
                    ans=max(ans,tail-head+1);
                    now-=abs(c[i-head+1]-c[i+head]);
                }
            }
        printf("%d\n",ans);
    }
    return 0;
}

hdu4123 BOb'race

我們看看,這道題就是求一個點在樹圖上所能到達的最遠距離,我們知道,這個距離就是到任意一條直徑兩端點的較大距離
這樣我們就得出了每個點的答案,然后用兩個單調隊列,一個存最大值的編號,一個存最小值的編號,O(n)輸出答案。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <queue>
using namespace std;
const int Maxn=50005;
struct Node{
	int lac,to,wg;
}edge[Maxn*2];
int n,m,cnt,h[Maxn],x,y,z,dis[Maxn],ans[Maxn],k;
deque<int> qmax;
deque<int> qmin;
bool vis[Maxn];
void insert(int x,int y,int z);
void find_dtr();
void build();
int dfs(int u);
void print();
void insert(int x,int y,int z){
	edge[cnt].lac=h[x];
	edge[cnt].to=y;
	edge[cnt].wg=z;
	h[x]=cnt++;
}
void build(){
	for(int i=1;i<n;i++){
		scanf("%d%d%d",&x,&y,&z);
		insert(x,y,z);
		insert(y,x,z);
	}
}
//我們通過深搜來記錄路徑
int dfs(int u) {
	vis[u]=1;
	int to=u;
	for(int i=h[u];i!=-1;i=edge[i].lac) {
		if(vis[edge[i].to]) continue;//雖然不可能。。。
		dis[edge[i].to]=edge[i].wg+dis[u];//我們得到to的路徑長
		int ret=dfs(edge[i].to);//我們就去下一個點,得到最大距離
		if(dis[to]<dis[ret]) to=ret;//修改一波
	}
	return to;//如果沒有葉子節點,返回其本身
}
//第一次寫的樹的直徑
//我們利用樹的直徑的性質。。。
//對於一條直徑
//一個點到別的點的最長路徑肯定是在兩端點的。。
//但是我們不知道到那個端點,於是做3次深搜。。
//前兩此的直徑,后兩次的路徑
void find_dtr() {
	memset(dis,0,sizeof dis);
	memset(vis,0,sizeof vis);
	int s=dfs(1);//返回距離u的最遠節點
	memset(dis,0,sizeof dis);
	memset(vis,0,sizeof vis);
	int t=dfs(s);//此時,已經有了一部分答案我們選擇memcpy
	memcpy(ans,dis,sizeof ans);
	memset(dis,0,sizeof dis);
	memset(vis,0,sizeof vis);
	s=dfs(t);//在重新搜回去
	
	//事實上,再有多個路徑的時候,前一個s可能不是這s,但是這s和t是直徑端點。
	for(int i=1;i<=n;i++) ans[i]=max(ans[i],dis[i]);
}
void print(){
	while(m--){
		//我們統計m次答案
    //我們做兩個單調隊列,一個記錄最大值的編號,一個記錄最小的編號
		scanf("%d",&k);
		int head=1,len=-1;
		for(int i=1;i<=n;i++){//枚舉嵬節點
			while(!qmin.empty()&&ans[i]<=ans[qmin.back()]) qmin.pop_back();//卻在前面越小
			while(!qmax.empty()&&ans[i]>=ans[qmax.back()]) qmax.pop_back();//越在前面越大
			qmin.push_back(i);
			qmax.push_back(i);
      while(ans[qmax.front()]-ans[qmin.front()]>k){
				head++;
				if(qmax.front()<head) qmax.pop_front();
				if(qmin.front()<head) qmin.pop_front();
			}
			len=max(len,i-head+1);
		}
		while(!qmin.empty()) qmin.pop_front();
		while(!qmax.empty()) qmax.pop_front();
		printf("%d\n",len);
	}
}
int main() {
	freopen("Bob.in","r",stdin);
	while(1){
		scanf("%d%d",&n,&m);
		if(n==0&&m==0) break;//程序結束
		memset(h,-1,sizeof h);cnt=0;//初始化
		build();//建樹
		find_dtr();//找直徑
		print();//單調隊列輸出答案
	}
	return 0;
}

其實,有些改變的題還有很多 像CF那道刪數,就要做個鄰接表

哼唧


免責聲明!

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



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