1. 程式人生 > 實用技巧 >單調佇列:從滑動視窗到NOIp2016-蚯蚓

單調佇列:從滑動視窗到NOIp2016-蚯蚓

  • 先附上網址

https://www.luogu.com.cn/problem/P1886

一道經典題目了。之前就做過(2019年山東省夏令營,2019-07-20 09:14,看來是在禮堂聽課時做的
閒話不多說就是單調佇列,一種資料結構,維護兩個方面:

為了懶,當然用stl:deque啦(其實是head==tail還是head>tail搞不清楚)

不過用st表也能AC,需要注意卡常數,程式碼如下:

#include<cstdio>
#include<cmath>
#define min(a,b) (a<b?a:b)
#define max(a,b) (a>b?a:b)
int n,k,a[1000006],st[1000006][22];
inline int read()//不快讀的話過得很勉強,快讀就比較輕鬆。另外不要引用isdigit()函式,會變慢
{
	int x=0,f=1;char ch=getchar();
	while (ch>'9'||ch<'0'){if (ch=='-') f=-1;ch=getchar();}
	while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
int main(){
    scanf("%d%d",&n,&k);

    for(int i=1;i<=n;++i){
        //scanf("%d",a+i);
        st[i][0]=read();
    }
    for(int j=1,maxx;j<=20;++j){
        maxx=n-(1<<j)+1;
        for(int i=1;i<=maxx;++i)
        st[i][j]=min(st[i][j-1],st[i+(1<<j-1)][j-1]);
    }
    int len=log2(k);//直接用此函式的話,已試驗,輸入整數時無誤差
    for(int i=1;i<=n-k+1;++i)
    printf("%d ",min(st[i][len],st[i+k-(1<<len)][len]));//其實是(i+k-1)-(1<<len)+1
    
    printf("\n");
    
     for(int j=1,maxx;j<=20;++j){
        maxx=n-(1<<j)+1;
        for(int i=1;i<=maxx;++i)
        st[i][j]=max(st[i][j-1],st[i+(1<<j-1)][j-1]);
    }
    for(int i=1;i<=n-k+1;++i)
    printf("%d ",max(st[i][len],st[i+k-(1<<len)][len]));

    return 0;
}

接下來是蚯蚓這道題

https://www.luogu.com.cn/problem/P2827

/**
 * 蛐蛐國裡現在共有 n 只蚯蚓( n 為正整數)。
 * 每隻蚯蚓擁有長度,我們設第 i 只蚯蚓的長度為 a[i]
 * (i=1,2,...,n)
 * 並保證所有的長度都是非負整數
 * (即:可能存在長度為 0 的蚯蚓)。
*//**
 * 每一秒,神刀手會在所有的蚯蚓中,
 * 準確地找到最長的那一隻(如有多個則任選一個)
 * 將其切成兩半。
 * 神刀手切開蚯蚓的位置由常數 p
 * (是滿足 0 < p < 1 的有理數)
 * 決定,設這隻蚯蚓長度為 x,
 * 神刀手會將其切成兩隻長度分別為
 * ⌊px⌋ 和 x - ⌊px⌋ 的蚯蚓。
 * 特殊地,如果這兩個數的其中一個等於 0,
 * 則這個長度為 0 的蚯蚓也會被保留。
 * 此外,除了剛剛產生的兩隻新蚯蚓,
 * 其餘蚯蚓的長度都會增加 q(是一個非負整常數)。
*/

附上ckw大佬的分析(也是2019年夏令營的)

那麼思想的話已經很明顯了。正解也很多,文章最後貼一個正解做法(我們教練寫的)。這裡給一個騙分的做法:

NOIp部分分給的非常足!且資料一般不會出現極限資料,必須卡常之類的

這就給我們快速拿 部分分 提供了可乘之機

我們快速寫一個不是正解但是一定對的程式碼然後瘋狂卡常,如下:
考慮到每次取最大蚯蚓,且不斷插入/刪除,我們用一個優先佇列來維護
其他的就是照著題面模擬了。需要注意的是每次插入新的蚯蚓時要減去time×q以“拉平”,
換言之,其他蚯蚓都少加了time×q,切的這條蚯蚓切的時候是真實的長度,切後要變回“虛假”的長度
其他的按照體面輸入輸出就完了。開不開O2優化都是85分,優化作用也有限。

#include<cstdio>
#include<queue>
using namespace std;
int _X,_F,CH;
inline int read(){//read
    _X=0;_F=1;CH=getchar();
    while(CH>57||CH<48){if(CH=='-')_F=-1;CH=getchar();}
    while(CH>=48&&CH<=57){_X=_X*10+CH-48;CH=getchar();}
    return _X*_F;
}

priority_queue<int/*,vector<int>,greater<int> */>pq;
int n,m,q,u,v,t/*,a[100005]*/;//含義見題目
int longest,shorter;//變數名起的長一點便於理解
int main(){
    n=read();m=read();q=read();u=read();v=read();t=read();
    for(int i=1;i<=n;++i)
    {
        //a[i]=read();
        pq.push(read());
    }
    for(int time=1;time<=m;++time)
    {
        longest=pq.top();pq.pop();
        longest+=(time-1)*q;//本輪他並沒有變長
        if(time%t==0)printf("%d ",longest);
        shorter=longest*(long long)u/v;//這個long long常數很大,但是必須
        pq.push(shorter-time*q);pq.push(longest-shorter-time*q);
//寫這篇文章時我想到了把time*q存起來,但是該拿的85分到手了,剩下幾個點真的沒什麼用
    }
    putchar('\n');
    int addlen=m*q;//<2^31
    for(int i=1;i<=n+m;++i)
    {
        if(i%t){pq.pop();continue;}
        longest=pq.top();pq.pop();
        printf("%d ",longest+addlen);
    }
    
    return 0;
}

這是一份正解,三個單調佇列,含義如ckw所述。題解滿大街都是,貼個我們教練的:

 # include<bits/stdc++.h>
 #define rep(i,n) for(int i=1;i<=n;++i) 

  using namespace std;
  inline int read()
  {
    int x=0,f=1;
    char ch;ch=getchar();
    while(ch<48 ||ch>57){if(ch=='-') f=-1;ch=getchar(); }
    while(ch>=48&&ch<=57) {x=x*10+ch-48;ch=getchar();}
    return x*f;
  }
  const int  N=1e5+5;
  const int  M=1e7;
  int a[3][M],cut[M],h[3],t[3];//a[0] 原數列  a[1]  砍斷後長的段  a[2]砍斷後短的段
  // cut [] 記錄被砍斷的蚯蚓  h[0] h[1] h[2] 代表 a[0] a[1] a[2]頭指標 t[]分別是尾指標 
  int n,m,q,u,v,T,inf;
   bool cmp(int &a,int &b) {return a>b;}
 int main()
 {  //freopen("in.txt","r",stdin);
   cin>>n>>m>>q>>u>>v>>T;
    
   for(int i=0;i<=2;++i) rep(j,M)a[i][j]=-1e9;
   inf=-1e9;
 
  h[0]=h[1]=h[2]=1; cut[0]=0;
   rep(i,n) a[0][i]=read();
   sort(a[0]+1,a[0]+1+n,cmp);// 首先讓a[0] 從大到小 有序 

   double per=u*1.0/v;
   rep(i,m)// 開始砍蚯蚓 
   {
     int temp,maxn;
      if(a[0][h[0]]>=a[1][h[1]])  maxn=a[0][h[0]],temp=0;
      else maxn=a[1][h[1]],temp=1;
      if(a[2][h[2]]>maxn) maxn=a[2][h[2]],temp=2;
    // 找到a[0]  a[1] a[2] 隊首最大的 就是 應該被砍的蚯蚓 
      h[temp]++;//被砍的蚯蚓被彈出隊首 
      
      if(i%T==0)cut[++cut[0]]=maxn+(i-1)*q;//被砍的蚯蚓應當加上增長補償,每一秒都增長q 
      // 接下來把砍斷的蚯蚓 按大小存在a[1]  a[2] 佇列 
      int small=(maxn+(i-1)*q)*per,big=maxn+(i-1)*q-small;
     if(big<small) swap(big,small);
       a[1][++t[1]]=big-i*q,a[2][++t[2]]=small-i*q;
       
   }
   //按要求輸出  事後我想cut陣列也許沒有必要  直接輸出即可 
  rep(i,cut[0]) cout<<cut[i]<<" ";   cout<<endl;
 
  
  int left=(n+m)/T,cnt=0;
  // 後來身負洪荒之力人來了  蚯蚓不在增長  只需要把序列輸出即可 
  while(left)
  {
   int temp,maxn=inf;
      if(a[0][h[0]]>=a[1][h[1]])  maxn=a[0][h[0]],temp=0;
      else maxn=a[1][h[1]],temp=1;
      if(a[2][h[2]]>=maxn) maxn=a[2][h[2]],temp=2;   
       
      h[temp]++;
      if(++cnt%T==0)cout<<maxn+m*q<<" ",left--;

  }
  
   
   
 return 0;
 }

還有一位同學因常數過大被卡所以一定快讀

文末總結:
一些看似高階的題目其實可能有簡單(嗎?)的做法,尤其是單調佇列,好幾次出現了
所以還是那句話,基礎演算法靈活掌握。
另外就是ckw教的經驗吧,先考慮部分分的限制條件,不行就先自己加一個,在這種受限的背景下題目還會含有什麼限制條件,
再推廣到更高分看是不是還成立。我覺得部分分就挺香的了