24暑假集訓day2上午

Yantai_YZY發表於2024-08-03

上午

內容:基礎資料結構

1. 連結串列

分類:單向和雙向

單向:當前連結串列只指向下一個元素

雙向:對於每個元素,記錄其前面一個元素,也記錄其後面一個元素。

注意:連結串列不建議使用 STL 的某些元素進行替代,手寫連結串列更為方便。

1. 單向連結串列

做法:維護每個元素編號,然後維護 nx 指標,表示當前元素的下一個元素是誰加入和刪除都是方便的。

手寫的單向連結串列:

void ins(int x,int y){
	int to=nx[x];
	nx[x]=y,nx[y]=to;
}
void erase(int x){
	int to=nx[x];
	nx[x]=nx[to];
}

T1 單向連結串列

問題簡述:如題

思路:我諤諤, 直接模擬

std:

#include <iostream>
using namespace std;
const int MAXN = 1e6 + 10;
int nx[MAXN];
int main(){
	int q;
	cin >> q;
	for(int i = 1;i <= q;i++){
		int op;
		cin >> op;
		if(op == 1){
			int x, y;
			cin >> x >> y;
			int a;
			a = nx[x];
			nx[x] = y;
			nx[y] = a;
		}else if(op == 2){
			int x;
			cin >> x;
			cout << nx[x] << '\n';
		}else{
			int x;
			cin >> x;
			int a = nx[x];
			nx[x] = nx[a];
		}
	}
	return 0;
}

記錄


前項星

本質:多個單項鍊表組成,維護鏈頭陣列,然後可以支援每個點加邊。

struct nod{
	int next,to;
}e[xx*2];
int cnt,h[xx];
void add(int x,int y){
	cnt++;
	e[cnt]={h[x],y};
	h[x]=cnt;	
}
void dfs(int x,int y){
	for(int i=h[x];i;i=e[i].next){
		dfs(e[i].to,x);
	}
}

T2 單向連結串列

思路:直接複製上述程式

#include<bits/stdc++.h>
using namespace std;
const int MAXN=100005;
struct Node{
	int next,to;
}e[MAXN*2];
int n,m,cnt,h[MAXN];
void add(int x,int y){
	cnt++;
	e[cnt]={h[x],y};
	h[x]=cnt;
}
int ans[MAXN],id;
void dfs(int x){
	if(!ans[x])ans[x]=id;
	else return;
	for(int i=h[x];i;i=e[i].next)dfs(e[i].to);
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int a,b;
		cin>>a>>b;
		add(b,a); 
	}
	for(int i=n;i>=1;i--)id=i,dfs(i);
	for(int i=1;i<=n;i++)cout<<ans[i]<<' ';
	return 0;
}

記錄


2. 雙向連結串列

做法:每個元素維護前驅 \(\text{pr}\) 和後繼 \(\text{nx}\) 兩個陣列,可以實現動態增刪的操作

手寫的雙向連結串列:

void ins(int x,int y){
	int to=nx[x];
	nx[x]=y,pr[to]=y;
	pr[y]=x,nx[y]=to;
}
void era(int x){
	int L=pr[x],R=nx[x];
	nx[L]=R,pr[R]=L;
}

2. 棧

做法:維護一個序列,每次從末端加入元素,然後末端彈出元素

具體維護:使用一個陣列,維護一個 \(\text{tp}\) 指標進行處理

手寫的棧:

int stk[xx],tp;
void push(int x){
	stk[++tp]=x;
}
int top(){
	return stk[tp];	
}
void pop(){
	--tp;
}

STL 的棧:

stack<int>stk;
stk.push(1);
stk.pop();
cout<<stk.top()<<"\n";
cout<<stk.size()<<"\n";

T3 棧

模板題直接用 STL。

#include <iostream>
#include <stack>
#include <cstring>
using namespace std;
#define int unsigned long long
stack<int>stk;
void a(int n){
	for(int i = 1;i <= n;i++){
		string op;
		cin >> op;
		if(op == "push"){
			int x;
			cin >> x;
			stk.push(x);
		} else if(op == "pop"){
			if(!stk.empty()){
				stk.pop();
			} else {
				cout << "Empty" << '\n';
			}
		} else if(op == "query"){
			if(!stk.empty()){
				cout << stk.top() << '\n';
			} else {
				cout << "Anguei!" << '\n';
			}
		} else if(op == "size"){
			cout << stk.size() << '\n';
		}
	}
}
int t, n;
signed main(){
	cin >> t;
	for(int i = 1;i <= t;i++){
		int n;
		cin >> n;
		a(n);
		while(!stk.empty()){
			stk.pop();
		}
	}
	/*
	stack<int>stk;
	stk.top();
	stk.pop();
	cout << stk.top() << '\n';
	cout << stk.size() << '\n';
	*/
	return 0;
}

記錄

單調棧

實質:用棧維護了單調的結構

T3 棧

問題簡述:給定一個長度為 \(n\) 的數列 \(a_i\)。然後,對於每個元素,找到在這個元素之後,第一個大於當前元素的下標。

方法:從後往前列舉,棧內維護單調的下標,滿足這些下標的值是遞減的

關鍵:保留對當前狀態最優的資訊。

#include<bits/stdc++.h>
using namespace std;
int n,a[3000005];
int top,ans[3000005],maxn;
struct node{
    int val,i;
};
stack<node>sta;
stack<int>p; 
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i]; 
    }
    for(int i=n;i>=1;i--){
        int vis=0;
        while(!sta.empty()){
            if(a[i]<sta.top().val){
                p.push(sta.top().i);
                vis=1;
                break;
            }
            else sta.pop();
        }
        if(!vis)p.push(0);
        sta.push((node){a[i],i});
    }
    for(int i=1;i<=n;i++){
        cout<<p.top()<<' ';
        p.pop();
    }
}

記錄


3. 佇列

方法:後進先出

手寫的佇列:

int q[xx],l,r;
void push(int x){
	q[++r]=x;
}
int front(){
	return q[l];
}
void pop(){
	l++;
}

STL 佇列:

queue<int>q;
q.push(1);
q.pop();
cout<<q.front()<<"\n";
cout<<q.size()<<"\n";

T4 佇列

模板題直接用 STL。

#include <iostream>
#include <queue>
using namespace std;
queue<int> q;
int main(){
	int n;
	cin >> n;
	for(int i = 1;i <= n;i++){
		int op;
		cin >> op;
		if(op == 1){
			int x;
			cin >> x;
			q.push(x);
		} else if(op == 2){
			if(!q.empty()){
				q.pop();
			} else {
				cout << "ERR_CANNOT_POP" << '\n';
			}
		} else if(op == 3){
			if(!q.empty()){
				cout << q.front() << '\n';
			} else {
				cout << "ERR_CANNOT_QUERY" << '\n';
			}
		} else {
			cout << q.size() << '\n';
		}
	}
	return 0;
}

單調佇列

定義:權值是遞減的(求目前最大值),但是 \(\text{id}\) 是遞增的(有 \(\text{id}\) 大於某個位置的限制),有單調性質。

關鍵:保留對於當前而言更優的一個資訊。

T5 單調佇列

方法:

  1. 判斷隊首元素是否超出範圍,如果是,則隊首元素出隊;

  2. 判斷隊尾元素是否滿足要求(例如求區間最大值,要求則為隊尾元素小於插入的元素),如果是,則隊尾元素出隊,返回步驟 \(2\);如果不是,則進入下一步;

  3. 將元素插入隊尾。

std:

#include <bits/stdc++.h>
using namespace std;
int a[1000005];
int q[1000005];
int l, r;
int read() {
    char c;
    int w = 1;

    while ((c = getchar()) > '9' || c < '0')
        if (c == '-')
            w = -1;

    int ans = c - '0';

    while ((c = getchar()) >= '0' && c <= '9')
        ans = (ans << 1) + (ans << 3) + c - '0';

    return ans * w;
}
int m;
int main() {
    int n;
    n = read();
    m = read();

    for (int i = 1; i <= n; i++) {
        a[i] = read();
    }

    l = r = 1;
    q[1] = 1;

    for (int i = 1; i <= n + 1; i++) {
        while (l <= r && q[l] < i - m)
            l++;

        if (i > m)
            printf("%d ", a[q[l]]);

        while (l <= r && a[q[r]] >= a[i])
            r--;

        q[++r] = i;
    }

    l = r = 1;
    q[1] = 1;
    cout << endl;

    for (int i = 1; i <= n + 1; i++) {
        while (l <= r && q[l] < i - m)
            l++;

        if (i > m)
            printf("%d ", a[q[l]]);

        while (l <= r && a[q[r]] <= a[i])
            r--;

        q[++r] = i;
    }

    return 0;
}

記錄


4. 佇列

方法:維護一個序列,每次加入一個元素,詢問當前序列中最小的元素,然後刪除最小元素。

具體維護:一般維護的稱為二叉堆,即維護一個結構,支援上述處理過程。

每個節點 \(i\) 儲存一個權值,左兒子是 \(2 \cdot i\),右兒子是 \(2 \cdot i + 1\)

手寫的堆:

int hp[xx],sz;
void push(int val){
	int id=++sz;
	hp[id]=val;
	while(id!=1){
		int to=id/2;
		if(hp[id]<hp[to])swap(hp[id],hp[to]);
		id=to;
	}
}
void pop(){
	hp[1]=hp[sz],--sz;
	int id=1;
	while((id*2)<=sz){
		int L=id*2,R=id*2+1,ty=L;
		if(R<=sz&&hp[R]<hp[ty])ty=R;
		if(hp[ty]<hp[id])swap(hp[ty],hp[id])
		id=ty;
	}
}

STL 堆:

priority_queue<int>q;//大根堆!
q.push(1),q.pop();
cout<<g.top()<<"\n";
cout<<q,size()<<\n";

priority_queue<int,vector<int>,greater<int>>q;//小根堆!
q.push(1),q.pop();
cout<<g.top()<<"\n";
cout<<q.size()<<"\n";

T5 堆

思路:模板是小根堆,直接用STL

#include<iostream>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int MAXN=1000005;
priority_queue<int,vector<int>,less<int> > h;
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        int op;
        cin>>op;
        if(op==1){
            int x;
            cin>>x;
            h.push(-x);
        }else if(op==2)cout<<-h.top()<<'\n';
        else h.pop();
    }
    return 0;
}

記錄


對頂堆

T6 堆

問題簡述:一個序列,我們每次加入一個元素,或者進行詢問。
維護一個初始指標 \(i=0\),每次詢問的時候將 \(i=i+1\) 然後詢問第 \(i\) 小的值是多少。

思路:使用兩個堆,把序列分為前半和後半,分界點位置就是我們嘗試輸出的值。

std:

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200010;
priority_queue<int>q1, q2;
int a[MAXN], u[MAXN];
int main(){
    int n,m;
    cin >> n >> m;
    for(int i=1;i<=n;i++)cin>>a[i];
    for(int i=1;i<=m;i++){
        cin>>u[i];
        for(int l=u[i-1]+1;l<=u[i];l++){
            q2.push(a[l]);
        }
        while(q1.size()&&q2.size()&&-q1.top()<q2.top()){
            q1.push(-q2.top());
            q2.pop();
        }
        while(q2.size()<i){
            q2.push(-q1.top());
            q1.pop();
        } 
        while(q2.size()>i){
            q1.push(-q2.top());
            q2.pop();
        }
        cout<<q2.top()<<endl;
    }
    return 0;
}

記錄


5. 哈夫曼樹

本質:在堆的基礎上的一點點貪心的擴充套件

性質:
每個數貢獻的次數是他到根的邊數。
數大的貢獻較少,即經過的邊數較少。
如果把邊順次標號為 \(0, 1\),則每個葉子到根的一個字串稱為哈夫曼編碼。

T7 合併果子

問題簡述:有 \(n\) 堆果子,第 \(i\) 堆初始為 \(a_i\) 大,然後你需要做 \(n − 1\) 次操作,
每次選擇兩堆果子,將其合併為一堆,
即刪除原來兩堆 \(i, j\) 變成新堆 \(a_i + a_j\),並消耗新堆大小的體力,問:最少消耗體力。

std:

#include <iostream>
#include <queue>
using namespace std;
int n, x, ans;
priority_queue<int, vector<int>, greater<int> >q;
int main(){
	cin >> n;
	for(int i = 1;i <= n;i++){
		cin >> x;
		q.push(x);
	}
	while(q.size() >= 2){
		int a = q.top();
		q.pop();
		int b = q.top();
		q.pop();
		ans += a + b;
		q.push(a + b);
	}
	cout << ans << endl;
	return 0;
}

記錄


哈夫曼樹擴充套件

思路:選擇最小的兩堆合併,並一直執行這個過程

多叉的哈夫曼樹同樣涉及到一個補 \(0\) 的貪心思想。
具體的,補其 \(0\) 使得 \(n − 1 \bmod (k − 1) = 0\)

T8 荷馬史詩

問題簡述:給定 \(n\) 個單詞的出現次數,請你用字符集大小為 \(k\) 的字串來替換每個單詞,使得每個單詞的出現次數乘對應字串的長度最小,你需要滿足要求:對於任意兩個單詞對應的字串不應該有字首包含關係,比如 \(\texttt{abbc}\) 字首包含 \(\texttt{abb}\)

思路:注意到,沒有字首包含關係完全對應了哈夫曼編碼,而我們最最佳化次數正好是哈夫曼樹的最小的貢獻
所以我們建立擴充套件的 \(k\) 叉哈夫曼樹即可得到答案。

std:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int read()
{
	char c;
	int w=1;
	while((c=getchar())>'9'||c<'0')if(c=='-')w=-1;
	int ans=c-'0';
	while((c=getchar())>='0'&&c<='9')ans=(ans<<1)+(ans<<3)+c-'0';
	return ans*w;
}
priority_queue<pair<int,int> >q;
int a[5000005];
int n;
int k;
signed main(){
	n=read();
	k=read();
	for(int i=1;i<=n;i++)
	{
		a[i]=read();
		q.push(make_pair(-a[i],-1));
	}
	while((n-1)%(k-1))n++,q.push(make_pair(0,-1));
	int anss=0;
	for(int i=1;i<=(n-1)/(k-1);i++)
	{
		int ans=0;
		int maxx=0;
		for(int j=1;j<=k;j++)
		{
			ans+=(-q.top().first);
			maxx=max(maxx,-q.top().second);
			q.pop();
		}
		anss+=ans;
		q.push(make_pair(-ans,-maxx-1));
	}
	cout<<anss<<endl<<-q.top().second-1<<endl;
	return 0;
}

記錄