構文解析アルゴリズムコンテストのテーマ(5):シンプルなデータ構造

「先進のアルゴリズムコンテストエントリー」(:このシリーズは、このアルゴリズムの教科書の拡張であるJingdongは 详细)、清華大学プレス
:PDFダウンロードhttps://github.com/luoyongjun999/code「追加情報」の1
推奨場合、ください連絡先:(1)QQ群567554289;(2)QQ、15512356の

  ちょうど学んだプログラミング言語を書かれたこの記事では、新しいプレーヤーのデータ構造を学んでいます。

  「データ構造」教科書、これらの要素は、一般に、線形テーブル(アレイ、リンクリスト)、スタックとキュー、ストリング、アレイ、及び一般多次元テーブル、ハッシュ、バイナリツリー、ビュー(メモリマップ、トラバーサル)ランキング。
  リンクリスト、スタック、キュー:本論文では、いくつかの簡単な、詳細なコードとデータ構造を行使する。
  本「への高度なアルゴリズムコンテストエントリー」では、文字列、バイナリツリーは、図、詳細な説明があり、たとえば、他のデータ構造とコードの練習のいくつかは、ここでは繰り返しません。

1つのリスト

  機能のリストである:任意のデータ要素記憶部は一組の直線状(連続的であってもよいし、連続的ではないかもしれないメモリセルのグループ)。リストは、基本的なデータ構造を理解し、操作が簡単であり、そのアクションは次のとおりです。初期設定、追加、トラバース、挿入、削除、検索、ソート、リリースなど。
  ロックウェル谷P1996を用いて、以下の例では、動的鎖、静的鎖、STLリスト実装の5種類を与え。単独でリストがリンクされ、二重にリンクされたリストがあります。大会では、通常の静的リストやSTLのリストで、エンコード速度をスピードアップします。
  本稿では、コードの5種類が、著者の詳細な順序、ロジックプロセスとまったく同じの後、完全に類似お互いを理解し、私たちは、習得が容易異なる実装、に焦点を当てることができます。
----------------------------------------------
ロスバレーP1996 HTTPS ://www.luogu.com.cn/problem/P1996
ジョセフの問題
対象の説明:nは、個々のサークル、隣の人から再びメートルの列のうち、個々のパケットの最初の数、人の数から始まり、そして1は、人の数をカウントオフし始め、誰もが判明するまで、その後メートルにサークルなどは、出力数の人々の輪を消してください。
O:入力二つの整数N、M。Nの整数出力ライン、シーケンス内の各コイルの出力人々の数。1≤m、n≤100。
サンプル入力と出力
入力
103
出力
36927185104
------------------------------- ---------------

1.1動的リスト

  教科書は、動的リストを話すだろう、それは一時的な割り当てリストノード、使用後にリリースされたノードのリストを取ります。そうすることで、時間にスペースを解放することの利点は、余分なメモリを使用しないでください。欠点は、それがエラーを起こしやすいということです。
  次のコードを実装は、動的な方法は、リンクリスト。

#include <bits/stdc++.h>
struct node{          //链表结构
    int data;
    node *next;
};

int main(){
    int n,m;
    scanf("%d %d",&n,&m);
    node *head,*p,*now,*prev;   //定义变量
    head = new node; head->data = 1; head->next=NULL; //分配第一个节点,数据置为1        
    now = head;                 //当前指针是头
    for(int i=2;i<=n;i++){
        p = new node;  p->data = i; p->next = NULL;  //p是新节点
        now->next = p;        //把申请的新节点连到前面的链表上
        now = p;              //尾指针后移一个
    }    
    now->next = head;            //尾指针指向头:循环链表建立完成

 //以上是建立链表,下面是本题的逻辑和流程。后面4种代码,逻辑流程完全一致。

    now = head, prev=head;      //从第1个开始数
    while((n--) >1 ){ 
        for(int i=1;i<m;i++){       //数到m,停下
            prev = now;             //记录上一个位置,用于下面跳过第m个节点
            now = now->next; 
        }
        printf("%d ", now->data);       //输出第m节点,带空格
        prev->next = now->next;         //跳过这个节点
        delete now;                     //释放节点
        now = prev->next;               //新的一轮        
    }
    printf("%d", now->data);            //打印最后一个,后面不带空格
    delete now;                         //释放最后一个节点
    return 0;
}

静的片方向リンクリスト構造を持つ1.2

  スペースの使用は保存されますが、エラーを起こしやすいているが、動的リストの上に、あなたは、スペースを割り当てるとフリーする必要があります。コンテストでは、メモリ管理を厳密に動的割り当てとのトラブルの解除の必要性を排除し、エンコード速度、一般的に静的割り当てをスピードアップするために、必要とされていません。事前に割り当てられたのリストを格納するために大規模なアレイを使用して、この静的リスト。
  静的リストは2つの方法があり、一つはほとんどリンクされたリスト構造、および動的リンクされたリスト構造を定義である;アレイ上に直接、一次元アレイ、リンクリスト操作を使用することです。
  三つの例は、本明細書に与えられた静的ための片方向リンク・リスト構造を静的ため、双方向リンクリスト構造、一次元アレイを有する静的一方向リンクリスト。
  以下は、静的リストの構造を実現するための方法です。

#include <bits/stdc++.h>
const int maxn = 105;        //定义静态链表的空间大小
struct node{                 //单向链表
    int id;
    //int data;   //如有必要,定义一个有意义的数据
    int nextid;
}nodes[maxn];

int main(){
    int n, m;
    scanf("%d%d", &n, &m);
    nodes[0].nextid = 1;
    for(int i = 1; i <= n; i++){
        nodes[i].id = i;
        nodes[i].nextid = i + 1;
    }
    nodes[n].nextid = 1;                     //循环链表:尾指向头

    int now = 1, prev = 1;                   //从第1个开始
    while((n--) >1){
        for(int i = 1; i < m; i++){          //数到m,停下
            prev = now;  
            now = nodes[now].nextid;
        }
        printf("%d ", nodes[now].id);        //带空格
        nodes[prev].nextid = nodes[now].nextid;  //跳过节点now,即删除now
        now = nodes[prev].nextid;            //新的now
    }    
    printf("%d", nodes[now].nextid);         //打印最后一个,后面不带空格
    return 0; 
}

1.3双方向は静止構造とリンクリスト

#include <bits/stdc++.h>
const int maxn = 105;
struct node{      //双向链表
    int id;       //节点编号
    //int data;   //如有必要,定义一个有意义的数据
    int preid;    //前一个节点
    int nextid;   //后一个节点
}nodes[maxn];

int main(){
    int n, m;
    scanf("%d%d", &n, &m);
    nodes[0].nextid = 1;
    for(int i = 1; i <= n; i++){  //建立链表
        nodes[i].id = i;
        nodes[i].preid = i-1;     //前节点
        nodes[i].nextid = i+1;    //后节点
    }
    nodes[n].nextid = 1;          //循环链表:尾指向头
    nodes[1].preid = n;           //循环链表:头指向尾

    int now = 1;                  //从第1个开始
    while((n--) >1){
        for(int i = 1; i < m; i++)     //数到m,停下
            now = nodes[now].nextid;
        printf("%d ", nodes[now].id);  //打印,后面带空格

        int prev = nodes[now].preid;   
        int next = nodes[now].nextid;
        nodes[prev].nextid = nodes[now].nextid;  //删除now
        nodes[next].preid = nodes[now].preid;   
        now = next;                    //新的开始
    }    
    printf("%d", nodes[now].nextid);   //打印最后一个,后面不带空格
    return 0; 
}

一方向静的リストの1.4一次元アレイ

  これは最も単純な実装です。一次元配列を定義\(ノード[] \)、\ (ノード[I] \)\(I \)は、ノードの値(ノード[I] \)\値は次のノードです。
  それはデータノードがある保存することができますので、それ以上の説明から分かるように、その使用環境にも限定されている\(I \)

#include<bits/stdc++.h>
int nodes[150];
int main(){   
    int n, m;
    scanf("%d%d", &n, &m); 
    for(int i=1;i<=n-1;i++)          //nodes[i]的值就是下一个节点
        nodes[i]=i+1;
    nodes[n]=1;                      //循环链表:尾指向头

    int now = 1, prev = 1;           //从第1个开始
    while((n--) >1){
        for(int i = 1; i < m; i++){   //数到m,停下
            prev = now;  
            now = nodes[now];         //下一个
        }
        printf("%d ", now);  //带空格
        nodes[prev] = nodes[now];     //跳过节点now,即删除now
        now = nodes[prev];            //新的now
    }    
    printf("%d", now);                //打印最后一个,不带空格
    return 0;
}

1.5 STLリスト

  人種やプロジェクトは、多くの場合、C ++ STLのリストを使用して。リストは二重にリンクされたリストであり、そのメモリ領域は、データがポインタによってアクセスされる、不連続であってもよい、かつ効率的に挿入された任意の場所を削除することができ、挿入および削除操作は時定数です。
  読者は、初期化、追加、トラバース、挿入、削除、検索、ソート、リリースの自分のリストに精通している[ 参照 ]

  以下は、ロス・バレーP1996のリストが達成されます。

#include <bits/stdc++.h>
using namespace std;

int main(){
    int n, m;
    cin>>n>>m;
    list<int>node;
    for(int i=1;i<=n;i++)         //建立链表
        node.push_back(i);     
    list<int>::iterator it = node.begin();
    while(node.size()>1){         //list的大小由STL自己管理
        for(int i=1;i<m;i++){     //数到m
             it++; 
             if(it == node.end()) //循环链表,end()是list末端下一位置
                it = node.begin();                                              
        }
        cout << *it <<"";
        list<int>::iterator next = ++it;
        if(next==node.end())  next=node.begin();  //循环链表
        node.erase(--it);         //删除这个节点,node.size()自动减1
        it = next;
    }
    cout << *it;
    return 0;
 }

1.6演習リスト

  :「安全プランを証明する」ベストセラー演習のリストにOJアドレス与えhttps://leetcode-cn.com/problemset/lcof/
  :これらの問題は、リストの練習で
  頭に尾からのリストを印刷するには、顔の質問に06-
  顔の質問22-リストは最後から二番目のk個のノード
  顔質問24反転リスト
  顔の質問は、二つの順序付けられたリスト25のマージ
  質問コピーの35面の複雑な鎖
  顔の質問52-二つの第一の共通ノードリスト
  インタビュー質問18は、削除しますノードのリスト

2キュー

  データアクセスモードキューは「FIFO」です。Dafanチーム食堂例えば、最初のサーブ最初に来ます。
  循環キューおよびキューチェーン:キューは、2つの方法で実装されています。


  チェーン・キューは、ポインタに接続された各ノードと、単一リンクリストの特別なケースとして見ることができます。
  二つのポインタの円形キュー、配列表、順次キュー要素を格納する連続するメモリ位置のセットは、それぞれ前方および後方キューヘッドおよびキューテール素子要素を示しています。FIFOキューはその記憶部に、フロントとリアが直進している、「チーム」であることから、すべてのストレージスペースを歩いてオーバーフローする可能性があります。この問題を解決するために、キューは循環キューのリングとして設計されています。
  主な問題は、キューを見つけることで、スタックは、遅いスタートからフィニッシュまでのルックアップが必要です。一部のアプリケーションでは、あなたは、そう(な最大数など)優先順位が最も高いことを、最初のアウトキューをプライオリティキューを使用することができます。
  キューは非常に単純な、そして多くの場合、固定サイズであるため、それは一般的に静的な配列キューへの競争、またはSTLキューの使用に使用されています。
  ここで、コードの2.1および2.2にそれぞれ所定のSTLキュー静的配列と二種類で、一例です。
----------------------------------------------
ロスバレーP1540 HTTPS ://www.luogu.com.cn/problem/P1540
機械翻訳
タイトルの説明 :訳語と意味を格納できるMのメモリユニット、それぞれがあります。既に現在のメモリに記憶されている単語の数がM-1、記憶部格納新語ソフトウェアが使用されない超えていない場合、ソフトウェアは、新しい単語の前にメモリに格納されるたびに、メモリMに格納されている場合単語は、単語のソフトウェアは、ユニットを解放し、入力する最初のメモリを消去し、新しい単語を格納します。
Nワードの英語記事の長さを想定しています。この記事を考えるとは、何回外部メモリ辞書を見つけるために必要な翻訳ソフトウェアを翻訳するには?その翻訳開始を想定して前に、メモリには言葉がありません。
入力 :2行の合計。各行は二つの数字の間のスペースで区切られています。
最初の二つは、正の整数M、長さNの行為、及び物品の記憶容量を表します。
記事の順序でN非負整数の2行目に、英語の単語の代わりに各番号(1000サイズ)。二つの言葉は、彼らが非負の整数を対応と同じです場合にのみ、同じ言葉で記事です。
出力 :調査の必要性のための辞書ソフトウェアの整数。
サンプル入力と出力
入力する
。3. 7
。1 1 2 5 4 4 1
出力
5
--------------------------------- -------------

2.1 STLキュー

  :STLキューの操作上の
  キュー Q; //は、などのint、float型、char型、などスタック、タイプのデータ型、定義
  キューにq個のプッシュ(項目);. //プット項目を
  q.front(); //は、チームの最初の要素を返しますが、削除しません
  )(q.pop; //最初のチームの要素の削除
  q.backを(); //戻り尾の要素
  q.sizeを(); //戻り要素の数
  q.emptyを(); //チェックキューが空であるかどうかを
  、以下のロス・バレーP1540コード、彼らは自分たちがキューを管理していないので、コードは非常に簡単です。
  メモリノートコードをチェックのない単語方法はありません。遅すぎる、場所を検索する場合、だけでなく、迅速でなく、ハッシュコードと簡単です。

#include<bits/stdc++.h>
using namespace std;

int hash[1003]={0};  //用hash检查内存中有没有单词,hash[i]=1表示单词i在内存中
queue<int> mem;      //用队列模拟内存

int main(){
    int m,n;
    scanf("%d%d",&m,&n);
    int cnt=0;          //查词典的次数
    while(n--){ 
    int en;
    scanf("%d",&en);    //输入一个英文单词
    if(!hash[en]){      //如果内存中没有这个单词
        ++cnt; 
        mem.push(en);   //单词进队列,放到队列尾部
        hash[en]=1;     //记录内存中有这个单词
        while(mem.size()>m){         //内存满了
            hash[mem.front()] = 0;   //从内存中去掉单词
            mem.pop();               //从队头去掉
            }
        }
    }
    printf("%d\n",cnt);
    return 0;
}

2.2手書き循環キュー

  下面是循环队列的模板。代码中给出了静态分配空间和动态分配空间两种方式。竞赛中用静态分配更好。

#include<bits/stdc++.h>
#define MAXQSIZE 1003      //队列大小
int hash[MAXQSIZE]={0};    //用hash检查内存中有没有单词

struct myqueue{                  
    int data[MAXQSIZE];    //分配静态空间
    /* 如果动态分配,就这样写: int *data;    */
    int front;             //队头,指向队头的元素
    int rear;              //队尾,指向下一个可以放元素的空位置

    bool init(){           //初始化
    /*如果动态分配,就这样写:
        Q.data = (int *)malloc(MAXQSIZE * sizeof(int)) ; 
        if(!Q.data) return false; */
        front = rear = 0;
        return true;
    }
    int size(){            //返回队列长度
        return (rear - front + MAXQSIZE) % MAXQSIZE;
    }
    bool push(int e){      //队尾插入新元素。新的rear指向下一个空的位置
         if((rear + 1) % MAXQSIZE == front ) return false;    //队列满
         data[rear] = e;
         rear = (rear + 1) % MAXQSIZE;
         return true;
    }
    bool pop(int &e){      //删除队头元素,并返回它
         if(front == rear) return false;   //队列空
         e = data[front];
         front = (front + 1) % MAXQSIZE;
         return true;
    }
}Q;  

int main(){
    Q.init();   //初始化队列
    int m,n;  scanf("%d%d",&m,&n);
    int cnt = 0;
    while(n--){ 
    int en;  scanf("%d",&en);    //输入一个英文单词
    if(!hash[en]){               //如果内存中没有这个单词
        ++cnt;
        Q.push(en);              //单词进队列,放到队列尾部
        hash[en]=1;
        while(Q.size()>m){       //内存满了
                int tmp;
                Q.pop(tmp);      //删除队头
            hash[tmp] = 0;       //从内存中去掉单词             
            }
        }
    }
    printf("%d\n",cnt);
    return 0;
}

2.3 单调队列

  前面讲的队列,是很“规矩”的,队列的元素都是“先进先出”,队头的只能弹出,队尾只能进入。有没有不那么“规矩”的队列呢?这就是单调队列,它有2个特征:
  (1)队列中的元素是单调有序的,且元素在队列中的顺序和原来在序列中的顺序一致;
  (2)单调队列的队头和队尾都能入队和出队。
  其中(1)是我们期望的结果,它是通过(2)来实现的。
  单调队列用起来非常灵活,在很多问题中应用它可以获得优化。简单地说是这样实现的:序列中的n个元素,用单调队列处理时,每个元素只需要进出队列一次,复杂度是O(n)。
  下面用两个模板题来讲解单调队列的应用,了解它们如何通过单调队列获得优化。注意队列中“删头、去尾、窗口”的操作。

2.3.1 滑动窗口


洛谷 P1886 https://www.luogu.com.cn/problem/P1886
滑动窗口 /【模板】单调队列
题目描述:有一个长为 n 的序列 a,以及一个大小为 k 的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。
例如:
The array is [1,3,-1,-3,5,3,6,7], and k = 3。

输入输出:输入一共有两行,第一行有两个正整数 n,k。 第二行 n 个整数,表示序列 a。输出共两行,第一行为每次窗口滑动的最小值,第二行为每次窗口滑动的最大值。
注意:\(1 ≤ k ≤ n ≤ 10^{6},a_i\in[-2^{31}, 2^{31}]\)
输入输出样例
输入
8 3
1 3 -1 -3 5 3 6 7
输出
-1 -3 -3 -3 3 3
3 3 5 5 6 7
----------------------------------------------
  这一题用暴力法很容易编程,从头到尾扫描,每次检查\(k\)个数,一共检查\(O(nk)\)次。暴力法显然会超时,这一题需要用\(O(n)\)的算法。
  下面用单调队列来求解,它的复杂度是\(O(n)\)的。
  在这一题中,单调队列有以下特征
  (1)队头的元素始终是队列中最小的;根据题目需要输出队头,但是不一定弹出。
  (2)元素只能从队尾进入队列,从队头队尾都可以弹出。
  (3)序列中的每个元素都必须进入队列。例如a进队尾时,和原队尾b比较,如果a≤b,就从队尾弹出b;弹出队尾所有比a大的,最后a进入队尾。入队的这个操作,保证了队头元素是队列中最小的。
  直接看上述题解可能有点晕,这里以食堂排队打饭为例子来说明它。
  大家到食堂排队打饭时都有一个心理,在打饭之前,先看看里面有什么菜,如果不好吃就走了。不过,能不能看到和身高有关,站在队尾的人如果个子高,眼光能越过前面队伍的脑袋,看到里面的菜;如果个子矮,会被挡住看不见。
  矮个子希望,要是前面的人都比他更矮就好了。如果他会魔法,他来排队的时候,队尾比他高的就自动从队尾离开,新的队尾如果仍比他高,也会离开。最后,新来的矮个子成了新的队尾,而且是最高的。他终于能看到菜了,让人兴奋的是,菜很好吃,所以他肯定不会走。
  假设每一个新来的魔法都比队列里的人更厉害,这个队伍就会变成这样:每个新来的人都能排到队尾,但是都会被后面来的矮个子赶走。这样一来,这个队列就会始终满足单调性:从队头到队尾,由矮到高。
  但是,让这个魔法队伍郁闷的是,打饭阿姨一直忙她的,顾不上打饭。所以排头的人等了一会儿,就走了,等待时间就是k。这有一个附带的现象:队伍长度不会超过k。
  输出什么呢? 每当新来一个排队的人,如果排头还没走,就跟阿姨喊一声,这就是输出。
  以上是本题的现实模型。
  下面举例描述算法流程,队列是{\(1,3,-1,-3,5,3,6,7\)},读者可以把数字想象成身高。以输出最小值为例,下面表格中的“输出队首”就是本题的结果。
元素进入队尾| 元素进队顺序| 队列| 窗口范围| 队首在窗口内吗?|输出队首|弹出队尾|弹出队首

|-: | -: | -: | -: | -:| -:| -:| -:|
|1| 1| {1}| [1]| 是| | | |
|3| 2| {1,3}| [1 2]| 是| | | |
|-1| 3| {-1}| [1 2 3]| 是| -1| 3,1| |
|-3| 4| {-3}| [2 3 4]| 是| -3| -1| |
|5| 5| {-3,5}| [3 4 5]| 是| -3| | |
|3| 6| {-3,3}| [4 5 6]| 是| -3| 5| |
|6| 7| {3,6}| [5 6 7]| -3否,3是| 3| | -3|
|7| 8| {3,6,7}| [6 7 8]| 是| 3| | |
  单调队列的时间复杂度:每个元素最多入队1次、出队1次,且出入队都是\(O(1)\)的,因此总时间是\(O(n)\)。题目需要逐一处理所有\(n\)个数,所以\(O(n)\)已经是能达到的最优复杂度。
  从以上过程可以看出,单调队列有两个重要操作:删头、去尾。
  (1)删头。如果队头的元素脱离了窗口,这个元素就没用了,弹出它。
  (2)去尾。如果新元素进队尾时,原队尾的存在破坏了队列的单调性,就弹出它。
  读者可以自己写一个单调队列,不过,一般用STL deque就好了。deque是双端队列,它的用法是:
  q[i]:返回q中下标为i的元素;
  q.front():返回队头;
  q.back():返回队尾;
  q.pop_back():删除队尾。不返回值;
  q.pop_front():删除队头。不返回值;
  q.push_back(e):在队尾添加一个元素e;
  q.push_front(e):在队头添加一个元素e。
  下面是P1886的代码[参考]

#include<bits/stdc++.h>
using namespace std;

int a[1000005];
deque<int>q;        //队列中的数据,实际上是元素在原序列中的位置

int main(){
    int n,m;
    scanf("%d%d",&n,&m);  
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);  
    for(int i=1;i<=n;i++){                       //输出最小值 
        while(!q.empty() && a[q.back()]>a[i])    //去尾
            q.pop_back(); 
        q.push_back(i);
        if(i>=m){                                //每个窗口输出一次
            while(!q.empty() && q.front()<=i-m)  //删头 
                q.pop_front();
            printf("%d ", a[q.front()]);
        }
    }
    printf("\n");

    while(!q.empty())  q.pop_front();            //清空,下面再用一次
    for(int i=1;i<=n;i++){                       //输出最大值 
        while(!q.empty() && a[q.back()]<a[i])    //去尾
            q.pop_back();     
        q.push_back(i);
        if(i>=m){
            while(!q.empty() && q.front()<=i-m)  //删头
                q.pop_front(); 
            printf("%d ", a[q.front()]);
        }
    }
    printf("\n");
    return 0;
}

2.3.2 最大子序和

  给定长度为n的整数序列A,它的“子序列”定义是:A中非空的一段连续的元素。子序列和,例如序列(6,-1,5,4,-7),前4个元素的和是6 + (-1) + 5 + 4 = 14。
  最大子序和问题,按子序列有无长度限制,有两种:
  (1)不限制子序列的长度。在所有可能的子序列中,找到一个子序列,该子序列和最大。
  (2)限制子序列的长度。给一个限制m,找出一段长度不超过m的连续子序列,使它的和最大。
  问题(1)比较简单,用贪心或DP,复杂度都是O(n)的。
  问题(2)用单调队列,复杂度也是O(n)的。通过这个例子,读者可以理解为什么单调队列能用于DP优化
  问题(1)不是本节的内容,不过为了参照,下面也给出题解。

1. 问题(1)的求解
  用贪心或DP,在O(n)时间内求解。例题是hdu 1003。
----------------------------------------------
hdu 1003 http://acm.hdu.edu.cn/showproblem.php?pid=1003
Max Sum
题目描述:给一个序列,求最大子序和。
输入:第1行是整数T,表示测试用例个数,1<=T<=20。后面跟着T行,每一行第1个数是N,后面是N个数,1<=N<=100000,每个数在[-1000, 1000]内。
输出:对每个测试,输出2行,第1行是"Case #:",其中"#"是第几个测试,第2行输出3个数,第1个数是最大子序和,第2和第3个数是开始和终止位置。
输入输出样例
输入
2
5 6 -1 5 4 -7
7 0 6 -1 1 -6 7 -5
输出
Case 1:
14 1 4

Case 2:
7 1 6
----------------------------------------------
题解1:贪心。 逐个扫描序列中的元素,累加。加一个正数时,和会增加;加一个负数时,和会减少。如果当前得到的和变成了负数,这个负数和在接下来的累加中,会减少后面的求和。所以抛弃它,从下一位置开始重新求和。
  hdu 1003的贪心代码:

#include<bits/stdc++.h>
using namespace std;
const int INF = 0x7fffffff;  
int main(){
    int t;  cin >> t;              //测试用例个数
    for(int i = 1; i <= t; i++){   
        int n; cin >> n;
        int maxsum = -INF;         //最大子序和,初始化为一个极小负数
        int start=1, end=1, p=1;   //起点,终点,扫描位置
        for(int j = 1; j <= n; j++){
            int a; cin >> a;       //读入一个元素
            int sum = 0;           //子序和
            sum += a;
            if(sum > maxsum){
                maxsum = sum;
                start = p;
                end = j;
            }
            if(sum < 0){      //扫到j时,前面的最大子序和是负数,那么从下一个j重新开始求和。
                sum = 0;
                p = j+1;
            }
        }
        printf("Case %d:\n",i);
        printf("%d %d %d\n", maxsum,start,end);
        if(i != t) cout << endl;
    }
    return 0;
}

题解2:DP。用dp[i]表示到达第i个数时,a[1]~a[i]的最大子序和。状态转移方程为dp[i] = max(dp[i-1]+a[i], a[i])。

#include<bits/stdc++.h>
using namespace std;

int dp[100005];                    //dp[i]: 以第i个数为结尾的最大值
int main(){
    int t; cin>>t;
    for(int i=1;i<=t;i++){
        int n; cin >> n;        
        for(int j=1;j<=n;j++)  cin >> dp[j];
        int start=1, end=1, p=1;   //起点,终点,扫描位置
        int maxsum = dp[1]; 
        for(int j=2; j<=n; j++){            
            if(dp[j-1] >= 0)        //dp[i-1]大于0,则对dp[i]有贡献
                 dp[j] = dp[j-1]+dp[j];      //转移方程
            else p = j;
            if(dp[j]> maxsum ) {
                maxsum = dp[j];
                start = p;
                end = j;
            }
        }
        printf("Case %d:\n",i);
        printf("%d %d %d\n", maxsum,start,end);
        if(i != t) cout << endl;
    }
}

2. 问题(2)的求解
  和2.3.1节的滑动窗口类似,可以用单调队列的“窗口、删头、去尾”来解决问题(2)。
  首先求前缀和s[i]。s[i]是a[1]~a[i]的和,算出所有的s[i]~s[n],时间是O(n)的。
  问题(2)转换为:找出两个位置i, k,使得s[i] - s[k]最大,i - k≤ M。对于某个特定的s[i], 就是找到与它对应的最小s[k]。如果简单地暴力检查,对每个i,检查比它小的m个s[k],那么总复杂度是O(nm)的。
  用单调队列,可以使复杂度优化到O(n)。其关键是,s[k]只进入和弹出队列一次。基本过程是这样的,从头到尾检查s[],当检查到某个s[i]时,在窗口m内:
  (1)找到最小的那个s[k],并检查s[i]-s[k]是不是当前的最小子序和,如果是,就记录下来。
  (2)比s[i]大的所有s[k]都可以抛弃,因为它们在处理s[i]后面的s[i']时也用不着了,s[i']-s[i]要优于s[i']-s[k],留着s[i]就可以了。
  这个过程用单调队列最合适:s[i]进队尾时;如果原队尾比s[i]大就去尾;如果队头超过窗口范围m就去头;而最小的那个s[k]就是队头。因为每个s[i]只进出队列一次,所以复杂度为O(n)。
  下面是代码。

#include<bits/stdc++.h>
using namespace std;

deque<int> dq;
int s[100005];
int main(){
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) scanf("%lld",&s[i]);
    for(int i=1;i<=n;i++) s[i]=s[i]+s[i-1];         //计算前缀和
    int ans = -1e8;
    dq.push_back(0);
    for(int i=1;i<=n;i++) {
        while(!dq.empty() && dq.front()<i-m)        //队头超过m范围:删头
            dq.pop_front();
        if(dq.empty()) 
            ans = max(ans,s[i]);
        else 
            ans = max(ans,s[i]-s[dq.front()]);       //队头就是最小的s[k]
        while(!dq.empty() && s[dq.back()] >= s[i])   //队尾大于s[i],去尾
            dq.pop_back();
        dq.push_back(i);
    }
    printf("%d\n",ans);
    return 0;
}

  在这个例子中,s[i]的操作实际上符合DP的特征。通过这个例子,读者能理解,为什么单调队列可以用于DP的优化。

2.4 队列习题

  (1)单调队列简单题[https://blog.csdn.net/sinat_40471574/article/details/90577147]:洛谷 P1440,P2032,P1714,P2629,P2422。

  (2)单调队列可以用于优化DP,例如多重背包的优化等。请参考:
https://blog.csdn.net/FSAHFGSADHSAKNDAS/article/details/52825227
  优化DP:洛谷 P3957、P1725。
  (3)二维队列:洛谷 P2776

3 栈

  栈的特点是“先进后出”。例如坐电梯,先进电梯的被挤在最里面,只能最后出来;一管泡腾片,最先放进管子的药片位于最底层,最后被拿出来。
  编程中常用的递归,就是用栈来实现的。栈需要用空间存储,如果栈的深度太大,或者存进栈的数组太大,那么总数会超过系统为栈分配的空间,就会爆栈,即栈溢出。这是递归的主要问题。
  本节的栈用到STL stack,或者自己写栈。为避免爆栈,需要控制栈的大小。

3.1 STL stack

  STL stack的有关操作:
  stack s;   //定义栈,Type为数据类型,如int,float,char等
  s.push(item);    //把item放到栈顶
  s.top();    //返回栈顶的元素,但不会删除。
  s.pop();    //删除栈顶的元素,但不会返回。在出栈时需要进行两步操作,先top()获得栈顶元素,再pop()删除栈顶元素
  s.size();    //返回栈中元素的个数
  s.empty();   //检查栈是否为空,如果为空返回true,否则返回false
  下面用一个例题说明栈的应用。
----------------------------------------------
hdu 1062 http://acm.hdu.edu.cn/showproblem.php?pid=1062
Text Reverse
翻转字符串。例如,输入“olleh !dlrow”,输出“hello world!”。
----------------------------------------------
  下面是hdu 1062的代码。

#include<bits/stdc++.h>
using namespace std;
int main(){
    int n;
    char ch;
    scanf("%d",&n);  getchar();
    while(n--){
        stack<char> s;
        while(true){
            ch = getchar();                   //一次读入一个字符
            if(ch==' '||ch=='\n'||ch==EOF){
                while(!s.empty()){
                    printf("%c",s.top());     //输出栈顶
                    s.pop();                  //清除栈顶
                }
                if(ch=='\n'||ch==EOF)  break;
                printf("");
            }
            else  
                s.push(ch);                   //入栈
        }
        printf("\n");
    }
    return 0;
}

3.2 手写栈

  自己写个栈,很节省空间。下面是hdu 1062的代码。

#include<bits/stdc++.h>
const int maxn = 100000 + 100;

struct mystack{
    char a[maxn];                         //存放栈元素,字符型
    int t = 0;                            //栈顶位置
    void push(char x){ a[++t] = x; }      //送入栈
    char top()       { return a[t]; }     //返回栈顶元素
    void pop()       { t--;         }     //弹出栈顶
    int empty()      { return t==0?1:0;}  //返回1表示空
}st;

int main(){
    int n;
    char ch;
    scanf("%d",&n);  getchar();
    while(n--){
        while(true){
            ch = getchar();                    //一次读入一个字符
            if(ch==' '||ch=='\n'||ch==EOF){
                while(!st.empty()){
                    printf("%c",st.top());     //输出栈顶
                    st.pop();                  //清除栈顶
                }
                if(ch=='\n'||ch==EOF)  break;
                printf("");
            }
            else  
                st.push(ch);                   //入栈
        }
        printf("\n");
    }
    return 0;
}

3.3 单调栈

  单调栈可以处理比较问题。单调栈内的元素是单调递增或递减的的,有单调递增栈、单调递减栈。
  单调栈比单调队列简单,因为栈只有一个出入口。
  下面的例题是单调栈的简单应用。
----------------------------------------------
洛谷 P2947 https://www.luogu.com.cn/problem/P2947
向右看齐
题目描述:N(1≤N≤10^5)头奶牛站成一排,奶牛i的身高是Hi(l≤Hi≤1,000,000)。现在,每只奶牛都在向右看齐。对于奶牛i,如果奶牛j满足i<j且Hi<Hj,我们说奶牛i仰望奶牛j。求出每只奶牛离她最近的仰望对象。
输入输出:第 1 行输入 N,之后每行输入一个身高 H_i。输出共 N 行,按顺序每行输出一只奶牛的最近仰望对象,如果没有仰望对象,输出 0。
输入输出样例
输入
6
3
2
6
1
1
2
输出
3
3
0
6
6
0
----------------------------------------------
题解:从后往前遍历奶牛,并用一个栈保存从低到高的奶牛,栈顶的奶牛最矮,栈底的最高。具体操作是:遍历到奶牛i时,与栈顶的奶牛比较,如果不比i高,就弹出栈顶,直到栈顶的奶牛比i高,这就是i的仰望对象;然后把i放进栈顶,栈里的奶牛仍然保持从低到高。
复杂度:每个奶牛只进出栈一次,所以是\(O(n)\)的。
  下面分别用STL stack和手写栈来实现。
(1)用STL stack实现

#include<bits/stdc++.h>
using namespace std;

int h[100001], ans[100001];
int main(){
    int n;
    scanf("%d",&n);
    for (int i=1;i<=n;i++)  scanf("%d",&h[i]);
    stack<int>st; 
    for (int i=n;i>=1;i--){
        while (!st.empty() && h[st.top()] <= h[i])  //栈顶奶牛没我高,弹出它,直到栈顶奶牛更高
            st.pop();
        if (st.empty())       //栈空,没有仰望对象
            ans[i]=0; 
        else                  //栈顶奶牛更高,是仰望对象
            ans[i]=st.top();
        st.push(i);
    }
    for (int i=1;i<=n;i++) 
        printf("%d\n",ans[i]);
    return 0;
}

(2)手写栈
  和3.2节几乎一样,只是改了栈元素的类型。

#include<bits/stdc++.h>
using namespace std;

const int maxn = 100000 + 100;
struct mystack{
    int a[maxn];                        //存放栈元素,int型
    int t = 0;                          //栈顶位置
    void push(int x){ a[++t] = x;  }    //送入栈
    int  top()      { return a[t]; }    //返回栈顶元素
    void pop()      { t--;         }    //弹出栈顶
    int empty()     { return t==0?1:0;} //返回1表示空
}st;

int h[maxn], ans[maxn];

int main(){
    int n;
    scanf("%d",&n);
    for (int i=1;i<=n;i++)  scanf("%d",&h[i]);
    for (int i=n;i>=1;i--){
        while (!st.empty() && h[st.top()] <= h[i])  //栈顶奶牛没我高,弹出它,直到栈顶奶牛更高
            st.pop();
        if (st.empty())       //栈空,没有仰望对象
            ans[i]=0; 
        else                  //栈顶奶牛更高,是仰望对象
            ans[i]=st.top();
        st.push(i);
    }
    for (int i=1;i<=n;i++) 
        printf("%d\n",ans[i]);
    return 0;
}

3.4 栈习题

  洛谷 P5788
  https://leetcode-cn.com/problemset/lcof/
    面试题09-用两个栈实现队列
    面试题30-包含min函数的栈
    面试题31-栈的压入、弹出序列
    面试题58-翻转单词顺序列(栈)

おすすめ

転載: www.cnblogs.com/luoyj/p/12409990.html