知识点系列——基础数据结构

1.1 链表

洛谷-P1996 约瑟夫问题

  • 我就直接模拟
void solve() {
    cin>>n>>k;

    int id=0;
    
    for(int i=1;i<=n;i++){
        int p=0;
        while(p<k){
            bool f=false;
            while(vis[id]){
                id++;
                f=1;
                if(id>n)id=1;
            }
            if(!f){
                id++;
                if(id>n)id=1;
                while(vis[id]){
                    id++;
                    if(id>n)id=1;
                }
            }
            p++;
        }
        cout<<id<<' ';
        vis[id]=1;
    }
}

洛谷-P1160 队列安排 list 做法

  • listinsert 函数,可以将某个数插入在某个位置的后面,这里的位置是 list<int>::iterator

  • 只不过为了优化,此时存储 list<int>::iterator 的所有位置,这样速度就会快很多。

int n,m;
list<int>lt;
list<int>::iterator elt[N];

void solve() {
    cin>>n;

    list<int>::iterator it;
    lt.push_back(1);
    elt[1]=lt.begin();

    FOR(i,2,n){
        int x,y;
        cin>>x>>y;
        if(y){
            it=elt[x];//然后这里的elt存储的是某个数前面的位置
            lt.insert(++it,i);//因为list插入都是在某个位置的后面插
            elt[i]=--it;//左闭右开
        }else{
            it=elt[x];
            lt.insert(it,i);
            elt[i]=--it;
        }
    }

    cin>>m;

    FOR(i,1,m){
        int x;
        cin>>x;
        if(elt[x]!=lt.end()){
            lt.erase(elt[x]);
        }
        elt[x]=lt.end();
    }

    for(auto x:lt)cout<<x<<' ';
}

1.2 队列

洛谷-P1540 机器翻译

  • 直接模拟。
void solve() {
    cin>>m>>n;

    deque<int>q;
    set<int>S;

    int ans=0;

    FOR(i,1,n){
        int x;
        cin>>x;
        if(!S.count(x)){
            S.insert(x);
            ans++;
            q.push_back(x);
            if(q.sz>m)S.erase(q.front()),q.pop_front();
        }
    }

    cout<<ans;

}

HDU-1003 Max Sum 连续和的dp

  • 这个就是 dp 问题,只不过我觉得用队列麻烦。
void solve() {
    cin>>n;

    FOR(i,1,n)cin>>w[i];

    int l=1,r=1,al=1,ar=1;

    int ans=-1e18;

    FOR(i,1,n){
        f[i]=max(f[i-1]+w[i],w[i]);
        if(f[i]==f[i-1]+w[i]){
            r=i;
        }else if(f[i]==w[i]){
            l=r=i;
        }

        if(f[i]>ans){
            ans=f[i];
            al=l,ar=r;
        }
    }

    cout<<"Case "<<T++<<":"<<endl;
    cout<<ans<<' '<<al<<' '<<ar<<endl;

}

洛谷-P1440 求m区间内的最小值 双端队列

  • 注意一下:这里是不包括 $i$ 的前面 $m$ 个的最小值,因此我把那个输出放在弹出队首之后和弹出队尾之前的操作。反之如果包括,则写在下面
void solve() {
    cin>>n>>m;

    FOR(i,1,n)cin>>w[i];

    deque<int>q;
    q.push_back(1);
    cout<<0<<endl;

    FOR(i,2,n){
        while(q.sz&&i-q.front()>m)q.pop_front();
    
        cout<<w[q.front()]<<endl;
        while(q.sz&&w[q.back()]>=w[i])q.pop_back();
        q.push_back(i);
    }
}

洛谷-P2032 扫描

  • 跟上一道题类似。
void solve() {
    cin>>n>>m;

    FOR(i,1,n)cin>>w[i];

    deque<int>q;
    q.push_back(1);

    FOR(i,2,n){
        while(q.sz&&i-q.front()>=m)q.pop_front();
        while(q.sz&&w[q.back()]<=w[i])q.pop_back();
        q.push_back(i);
        if(i>=m){//因为有包括该数的前k个数,所以必须写在下面,上一题是没有包括这个数,所以写在上面
            cout<<w[q.front()]<<endl;
        }
    }
}

洛谷-P1714 切蛋糕 前缀和

  • 就是把求最小值改成了前缀和,一样的。
void solve() {
    cin>>n>>k;

    FOR(i,1,n)cin>>w[i],s[i]=s[i-1]+w[i];

    deque<int>q;
    q.push_back(0);
    int ans=-1e18;

    FOR(i,1,n){
        while(q.sz&&i-q.front()>k)q.pop_front();
        while(q.sz&&s[i]<=s[q.back()])q.pop_back();
        q.push_back(i);
        ans=max(ans,s[i]-s[q.front()]);
    }
    cout<<ans<<endl;
}

总结一下(易错点):

  1. 如果是求前面的最小值的话,此时 deque 的初始加入的数不能是 0,必须是 1,如果是前缀和这样的,就可以是 0
  2. 如果是不包括 $i$ 的前面 $m$ 个的最小值,因此我把那个输出放在弹出队首之后和弹出队尾之前的操作。反之如果包括,则写在下面。

洛谷 - P2629 好消息,坏消息

  • 因为这道题有跨区间(就是从 $[k,n]$ $[1,k-1]$),那么可以扩展数组,那么此时这些区间的长度就是固定的了,那么我们就可以用单调队列来维护了。
void solve() {
    cin>>n;

    FOR(i,1,n)cin>>w[i],w[i+n]=w[i];

    deque<int>q;

    int m=n;
    n*=2;
    FOR(i,1,n){
        s[i]=s[i-1]+w[i];
    }
    q.push_back(1);

    int ans=0;

    FOR(i,2,n-1){
        while(q.sz&&i-q.front()>m)q.pop_front();
        while(q.sz&&s[i]<=s[q.back()])q.pop_back();
        q.push_back(i);
        if(i>=m){
            if(s[q.front()]-s[i-m]>=0)ans++;
        }
    }
    cout<<ans;
}

复制数组有的时候很重要,特别对于移动或跨区间问题上。

洛谷 - P2422 良好的感觉

  • 题目要求最大的值(区间内的最小值乘区间和的值),此时如果我们固定了最小的值,那岂不是很好算。根据这个思路,我们可以对于每一个数,看它往左最远能扩展到哪里,往右同理。这里就可以用栈来维护了。(悄悄地说一下,这道题好像也可用悬线法)
int n,w[N],s[N];//考虑一个值它能作为什么区间的最小值,即它能向两边扩展多少
int L[N],R[N];
int stk[N],top;

void solve() {
    cin>>n;

    FOR(i,1,n)cin>>w[i],s[i]=s[i-1]+w[i];

    int ans=0;

    FOR(i,1,n){
        while(top&&w[stk[top]]>=w[i]){
            top--;
        }
        L[i]=stk[top];
        stk[++top]=i;
    }

    top=0;
    stk[0]=n+1;

    FORD(i,1,n){
        while(top&&w[stk[top]]>=w[i])top--;
        R[i]=stk[top]-1;
        stk[++top]=i;
    }

    FOR(i,1,n)ans=max(ans,(s[R[i]]-s[L[i]])*w[i]);

    cout<<ans;
}

洛谷 - P1540 机器翻译

  • 题目叫我们查找查词典次数,查词典次数被定义为如果内存中有(此时我们可以用队列来维护内存,因为内存的大小是固定的),那么就可以增加答案了。
void solve() {
    cin>>m>>n;

    deque<int>q;
    set<int>S;

    int ans=0;

    FOR(i,1,n){
        int x;
        cin>>x;
        if(!S.count(x)){
            S.insert(x);
            ans++;
            q.push_back(x);
            if(q.sz>m)S.erase(q.front()),q.pop_front();
        }
    }

    cout<<ans;

}

洛谷 - P3957 跳房子 区间不固定的单调队列

  • 可以发现:这道题的弹跳距离是个范围。而且这道题具有二段性,也就是如果低于了这个值,那么可能跳不过去。这里得特别注意区间是个范围的单调队列的写法。
bool check(int x){
    int l,r;
    if(x>=d){
        l=1,r=x+d;
    }else{
        l=d-x,r=x+d;
    }

    VI f(n+n,-1e18);

    int res=0;
    int t=0;

    f[0]=0;
    deque<int>q;
    int j=0;

    FOR(i,1,n){
        while(i>j&&w[i].fi-w[j].fi>=l){//区间不固定,也就是区间是个大范围都是这么写
            while(q.sz&&f[q.back()]<=f[j])q.pop_back();
            q.push_back(j++);
        }
        while(q.sz&&w[i].fi-w[q.front()].fi>r)q.pop_front();
        if(q.sz)f[i]=f[q.front()]+w[i].se;
        if(f[i]>=k)return 1;
    }
    return 0;
}

void solve() {
    cin>>n>>d>>k;

    int s=0;

    FOR(i,1,n)cin>>w[i].fi>>w[i].se,s+=(w[i].se>0?w[i].se:0);

    if(s<k){
        cout<<-1;
        return;
    }

    int l=0,r=1e6;

    while(l+1!=r){
        int mid=l+r>>1;
        if(check(mid))r=mid;
        else l=mid;
    }
    
    cout<<r<<endl;
}

洛谷 - P1725 琪露诺

  • 这道题和上一道题类似,只不过得注意:只要她下一步的位置编号大于 $N$ 就算到达对岸。
void solve() {
    cin>>n>>L>>R;

    FOR(i,0,n)cin>>w[i];

    deque<int>q;

    memset(f,-0x3f,sizeof f);

    int j=0;

    f[0]=0;

    FOR(i,0,n*3){
        while(i>j&&i-j>=L){
            while(q.sz&&f[q.back()]<=f[j])q.pop_back();
            q.push_back(j++);
        }
        while(q.sz&&i-q.front()>R)q.pop_front();
        if(q.sz)f[i]=f[q.front()]+w[i];
    }

    int ans=-1e18;

    FOR(i,n,n*3){
        ans=max(ans,f[i]);
    }

    cout<<ans<<endl;
}

总结:区间是个范围的单调队列来维护最大/最小值的写法:

    int j=0;

    f[0]=0;

    FOR(i,0,n*3){
        while(i>j&&i-j>=L){//L,R是区间范围
            while(q.sz&&f[q.back()]<=f[j])q.pop_back();
            q.push_back(j++);
        }
        while(q.sz&&i-q.front()>R)q.pop_front();
        if(q.sz)f[i]=f[q.front()]+w[i];
    }

洛谷 - P2776 小组队列

  • 这道题存在在特定的位置上插入值,此时得考虑链表。

deque双端队列是一个很好的办法 deque的操作:

void push_front(const T& x):双端队列头部增加一个元素X

void push_back(const T& x):双端队列尾部增加一个元素x

void pop_front():删除双端队列中最前一个元素

void pop_back():删除双端队列中最后一个元素

iterator begin():返回向量头指针,指向第一个元素

iterator end():返回指向向量中最后一个元素下一个元素的指针(不包含在向量中)

int n,m,q;
int w[N];
list<int>lst;
deque<list<int>::iterator>pos[N];

void solve() {
    cin>>n>>m;
    
    FOR(i,0,n-1){
        cin>>w[i];
    }

    cin>>q;
    list<int>::iterator it;
    while(q--){
        string s;
        int x;
        cin>>s;
        if(s=="push"){
            cin>>x;
            if(pos[w[x]].sz){
                it=*(--pos[w[x]].end());
                it++;
                pos[w[x]].push_back(lst.insert(it,x));
            }else{
                lst.push_back(x);
                it=--lst.end();
                pos[w[x]].push_back(it);
            }
        }else{
            it=lst.begin();
            cout<<*it<<endl;
            pos[w[*it]].pop_front();
            lst.pop_front();
        }
    }
}

signed main() {
    int Task = 1;
    for (; Task; Task--) {
        solve();
    }
    return 0;
}

1.3 栈

HDU - 1062 Text Reverse

  • 模拟。
void solve() {
    getline(cin,s);
    s+='\n';
    stack<char>S;

    FOR(i,0,s.sz-1){
        if(s[i]!=' '&&s[i]!='\n'){
            S.push(s[i]);
        }else{
            while(S.sz){
                cout<<S.top();
                S.pop();
            }
            cout<<" ";
        }
    }
    cout<<endl;
}

洛谷 - P2947 Look Up S

  • 这里得从大到小去维护栈,因为奶牛是往右看的,右边的高,栈能维护从 $n$ 到 $i$ 最高的奶牛的下标。
void solve() {
    cin>>n;

    FOR(i,1,n)cin>>w[i];

    stack<int>S;

    VI ans(n+1,0);

    FORD(i,1,n){
        while(S.sz&&w[S.top()]<=w[i])S.pop();
        if(S.sz)ans[i]=S.top();
        S.push(i);
    }

    FOR(i,1,n)cout<<ans[i]<<endl;
}

洛谷 - P5788 单调栈

  • 和上一道类似。
void solve() {
    cin>>n;

    FOR(i,1,n)cin>>w[i];

    stack<int>S;

    VI ans(n+1,0);

    FORD(i,1,n){
        while(S.sz&&w[S.top()]<=w[i])S.pop();
        if(S.sz)ans[i]=S.top();
        S.push(i);
    }
    FOR(i,1,n)cout<<ans[i]<<' ';
}

洛谷 - P1449 后缀表达式

  • 就是维护两个栈,一个是符号栈,另一个是数字栈。符号栈还有优先级,优先级高的先运算。
void solve() {
    cin>>s;
    s.pop_back();
    stack<int>S;
    int res=0;
    FOR(i,0,s.sz-1){
        if(s[i]=='.'){
            S.push(res);
            res=0;
        }else if(s[i]>='0'&&s[i]<='9'){
            res=(res<<3)+(res<<1)+s[i]-'0';
        }else{
            int t=0;
            int a=S.top();S.pop();
            int b=S.top();S.pop();
            if(s[i]=='+')S.push(a+b);
            else if(s[i]=='-')S.push(b-a);
            else if(s[i]=='/')S.push(b/a);
            else S.push(b*a);
        }
    }
    cout<<S.top()<<endl;
}

洛谷 - P1739 表达式括号匹配

  • 这道题直接用栈维护,如果右括号多就不对,左括号到最后还有剩也不对。
void solve() {
    cin>>s;
    int p=0;
    stack<char>S;

    for(auto x:s){
        if(x=='(')S.push(x);
        if(x==')'){
            if(!S.sz){
                cout<<"NO"<<endl;
                return;
            }else{
                S.pop();
            }
        }
    }
    cout<<(!S.sz?"YES":"NO");
}

洛谷 - P1981 表达式求值

  • 这道题就是多了个中缀表达式转化成后缀表达式的操作以及判断括号是否匹配的操作。

具体细节可看代码。

string s;
stack<char>op;
stack<int>num;
map<char,int>S{{'+',1},{'-',1},{'*',2},{'/',2}};

void ovel(){
    int a=num.top();num.pop();
    int b=num.top();num.pop();
    char c=op.top();op.pop();
    if(c=='+')num.push((a+b)%10000);
    else if(c=='-')num.push((a-b)%10000);
    else if(c=='*')num.push((a*b)%10000);
    else num.push((a/b)%10000);
}

void solve() {
    cin>>s;
    
    FOR(i,0,s.sz-1){
        if(s[i]>='0'&&s[i]<='9'){
            int j=i;
            int res=0;
            while(j<s.sz&&s[j]>='0'&&s[j]<='9'){
                res=((res<<3)+(res<<1)+s[j]-'0')%10000;
                j++;
            }
            num.push(res);
            i=j-1;
        }else{
            while(op.sz&&S[op.top()]>=S[s[i]])ovel();
            op.push(s[i]);
        }
    }
    while(op.sz)ovel();
    cout<<num.top()<<endl;
}

洛谷 - P1175 表达式的转换

  • 这道题比较复杂,得考虑表达式树,这种做法是通法,因为它能输出,每一步的运算过程以及可以输出后缀前缀中缀表达式。很方便。

表达式树

建树

  1. 先找根:有加减找最后一个加减,有乘除找最后一个乘除,否则找第一个幂。

注意:括号内要看成一个整体(不然递归到后面会出错) 如果整个式子是形如 (xxxxx) 型要先把外括号去掉,且一定要对应。

  1. 递归。

  2. 我们得知道:

  • 树的结构。
  • 叶子结点的数值。
  • 每个节点的符号。(叶子结点就是" ")

参考

string s;
char ch[N];
int num[N],lf[N],rf[N],cnt;

void build(int l,int r){
    if(l==r){
        num[++cnt]=s[l]-'0';
        ch[cnt]=' ';
        return;
    }
    if(s[l]=='('){
        int t=1;
        FOR(i,l+1,r){
            if(s[i]=='(')t++;
            else if(s[i]==')')t--;
            if(!t){
                if(i==r)l++,r--;
                break;
            }
        }
    }

    int p,t=5;//t:+1-1*2/2^3

    FORD(i,l,r){
        if(s[i]==')'){
            int tt=1,j;
            for(j=i-1;j>=l;j--){
                if(s[j]==')')tt++;
                else if(s[j]=='(')tt--;
                if(!tt)break;
            }
            i=j;
            continue;
        }else if(s[i]<='9'&&s[i]>='0')continue;
        else{
            if((s[i]=='+'||s[i]=='-')&&t>1)p=i,t=1;
            else if((s[i]=='*'||s[i]=='/')&&t>2)p=i,t=2;
            else if((s[i]=='^')&&t>3)p=i,t=4;
        }
    }

    int x=++cnt;
    ch[x]=s[p];
    lf[x]=cnt+1;
    build(l,p-1);
    rf[x]=cnt+1;
    build(p+1,r);
}

int qmi(int a,int b){
    int res=1;
    while(b){
        if(b&1)res=res*a;
        a=a*a;
        b>>=1;
    }
    return res;
}

int calc(int a,int b,char c){
    if(c=='+')return a+b;
    if(c=='-')return a-b;
    if(c=='*')return a*b;
    if(c=='/')return a/b;
    return qmi(a,b);
}

void print(int u){
    if(ch[u]==' '){
        cout<<num[u]<<' ';
        return;
    }
    print(lf[u]),print(rf[u]);
    cout<<ch[u]<<' ';
}

void dfs(int x){
    if(ch[x]==' ')return;

    dfs(lf[x]);
    dfs(rf[x]);
    num[x]=calc(num[lf[x]],num[rf[x]],ch[x]);
    ch[x]=' ';
    print(1);
    cout<<endl;
}

void solve() {
    cin>>s;
    build(0,s.sz-1);
    print(1);
    cout<<endl;
    dfs(1);
}

1.4 二叉树与哈夫曼树

HDU - 2527 Safe Or Unsafe

  • 就是算huffman编码,因为一个字母的权值等于该字母在字符串中出现的频率。$WPL$ 的算法就是两个两个堆合并的过程。具体细节可看代码。
void solve() {
    cin>>t>>s;

    VI p(30,0);

    for(auto x:s){
        p[x-'a']++;
    }

    priority_queue<int,VI,greater<int>>q;

    FOR(i,0,25){
        if(p[i]){
            q.push(p[i]);
        }
    }

    int res=0;

    while(q.sz>1){
        int a=q.top();q.pop();
        int b=q.top();q.pop();
        q.push(a+b);
        res+=a+b;
    }

    if(!res)res=q.top();

    if(res<=t)cout<<"yes"<<endl;
    else cout<<"no"<<endl;

}

洛谷 - P1087 FBI 树

  • 就是递归(因为递归本身也就是栈)。要找后序遍历,就把输出放在后面即可(回溯位置)
char FBI(string p){
    if(p.sz>1){
        cout<<FBI(p.substr(0,p.sz/2));
        cout<<FBI(p.substr(p.sz/2,p.sz/2));
    }
    int t0=p.find('0'),t1=p.find('1');
    if(t0!=-1&&t1!=-1)return 'F';
    if(t0!=-1&&t1==-1)return 'B';
    if(t0==-1&&t1!=-1)return 'I';
}

void solve() {
    cin>>n>>s;
    cout<<FBI(s);
}

洛谷 - P1030 求先序排列

  • 已知中序与后序排列,求先序遍历。根据每种遍历的结构,比如先序遍历的结构是 根 左子树 右子树。那么可以很容易写出代码。
void dfs(string sa,string sb){
    if(sa.sz>0){
        cout<<sb[sb.sz-1];
        int t=sa.find(sb[sb.sz-1]);
        dfs(sa.substr(0,t),sb.substr(0,t));
        dfs(sa.substr(t+1),sb.substr(t,sa.sz-1-t));
    }
}

void solve() {
    cin>>a>>b;

    dfs(a,b);
}

洛谷 - P1305 新二叉树

  • 这就是建一颗树即可。
int n;
struct E{
    char l,r;
}w[N];

void dfs(char rt,char l,char r){
    if(!rt)return;
    cout<<rt;
    if(l)dfs(l,w[l].l,w[l].r);
    if(r)dfs(r,w[r].l,w[r].r);
}

void solve() {
    cin>>n;
    char c;
    FOR(i,1,n){
        string s;
        cin>>s;
        if(i==1){
            c=s[0];
        }
        if(s[1]!='*')w[s[0]].l=s[1];
        if(s[2]!='*')w[s[0]].r=s[2];
    }
    dfs(c,w[c].l,w[c].r);
}

洛谷 - P1229 遍历问题

  • 从当前遍历节点来看,如果不是链的话,受到左右节点影响,显然先序和后序遍历的子树开头和结尾两个字符不会相等。因此可以发现满足上述条件的唯一情况就是从当前节点开始的子树的是链的情况,接下来可以选择把子树放到左边,也可以选择放到右边,答案 $×2$ 即可。总的来说就是如果只有一个儿子的话,那么它可以是左儿子也可以是右儿子。
void solve() {
    cin>>a>>b;

    int t=0;

    FOR(i,0,a.sz-1){
        FOR(j,1,b.sz-1){
            if(a[i]==b[j]&&a[i+1]==b[j-1]){
                t++;
            }
        }
    }
    cout<<(1<<t);
}

洛谷 - P5018 对称二叉树

  • 只需要判断每个节点的子树是否为对称二叉树,然后计算该子树的节点个数就行了。
    当到节点 $x$ 时,需要满足 $v_{l_x}=v_{r_x}$。到节点 $y$ 和 $z$ 时,需要满足 $v_{l_y}=v_{r_z}$ 和 $v_{r_y}=v_{l_z}$。若其中 $l,r$ 其中一个的值为 $-1$ 或 $v$ 不相等,那么它就不是对称二叉树。
int n,w[N];
int l[N],r[N];
int siz[N];

void dfs1(int u){
    if(u==-1)return;
    siz[u]=1;
    dfs1(l[u]);
    siz[u]+=siz[l[u]];
    dfs1(r[u]);
    siz[u]+=siz[r[u]];
}

bool dfs2(int x,int y){
    if(x==-1&&y==-1)return true;
    if(x==-1||y==-1)return false;
    if(w[x]!=w[y])return false;
    return dfs2(l[x],r[y])&&dfs2(r[x],l[y]);
}

void solve() {
    cin>>n;

    FOR(i,1,n)cin>>w[i];
    FOR(i,1,n)cin>>l[i]>>r[i];

    dfs1(1);

    int ans=0;

    FOR(i,1,n){
        if(dfs2(i,i)){
            ans=max(ans,siz[i]);
        }
    }

    cout<<ans<<endl;
}

洛谷 - P5597 复读

string s;
struct E{
    int l,r;
}tr[N],tr2[N];//每次指令后都不可能返回祖先节点了
//到下一个灰色点之前必须先把上面部分全部访问完,否则就不可能回去了
int tot,p1,p2,tot2,ans=1e18;

int build(){
    int t=getchar()-'0';
    int p=++tot;
    if(t==1||t==3)tr[p].l=build();
    if(t==2||t==3)tr[p].r=build();
    return p;
}

void dfs(int p,int q){//合并树
    if(p==p1||q==p2)p2=q,q=1;
    if(tr[p].l){
        if(!tr2[q].l)tr2[q].l=++tot2;
        dfs(tr[p].l,tr2[q].l);
    }
    if(tr[p].r){
        if(!tr2[q].r)tr2[q].r=++tot2;
        dfs(tr[p].r,tr2[q].r);
    }
}

void calc(int u,int dep){
    p1=u,p2=0,tot2=1;//p1是原树,p2是合并树
    memset(tr2,0,sizeof tr2);
    dfs(1,tot2);
    ans=min(ans,2*(tot2-1)-dep);
    if(tr[u].l)calc(tr[u].l,dep+1);
    if(tr[u].r)calc(tr[u].r,dep+1);
}

void solve() {
    build();
    calc(1,0);
    cout<<ans<<endl;
}

洛谷 - P2168 荷马史诗

  • 典型的 $k$ 叉树。

对于 $k$ 叉哈夫曼树的求解,直观的想法是在贪心的基础上,改为每次从堆中去除最小的 $k$ 个权值合并。然而,仔细思考可以发现,如果在执行最后一次循环时,堆的大小在(2~k-1)之间(不足以取出 k 个),那么整个哈夫曼树的根的子节点个数就小于k。这显然不是最优解————我们任意取哈夫曼树中一个深度最大的节点,改为树根的子节点,就会使 $∑w_i\times l_i$ 变小。因此,我们应该在执行上述贪心算法之前,补加一些额外的权值为0的叶子节点,使叶子节点的个树满足(n-1)%(k-1)=0。

void solve() {
    cin>>n>>k;

    priority_queue<PII,VPII,GII>q;

    FOR(i,1,n){
        int x;
        cin>>x;
        q.push({x,0});
    }

    while((n-1)%(k-1)){
        q.push({0,0});//补节点
        n++;
    }

    int res=0;

    while(q.sz>1){
        int sum=0;
        int dep=0;
        FOR(i,1,k){
            sum+=q.top().fi;
            dep=max(dep,q.top().se);
            q.pop();
        }
        res+=sum;
        q.push({sum,dep+1});
    }

    cout<<res<<endl<<q.top().se<<endl;
}

1.5 堆

洛谷 - P3378 堆

  • 模拟
int n;
priority_queue<int, vector<int>, greater<int>> q;

void solve() {
    cin>>n;

    while(n--){
        int op,x;
        cin>>op;
        if(op==1){
            cin>>x;
            q.push(x);
        }else if(op==2){
            cout<<q.top()<<endl;
        }else{
            q.pop();
        }
    }
}

洛谷 - P1090 合并果子

  • 每次拿去两个较小的果子,这里就可以用堆来维护了。
int n,w[N];
priority_queue<int, vector<int>, greater<int>> q;

void solve() {
    cin>>n;

    FOR(i,1,n){
        cin>>w[i];
        q.push(w[i]);
    }

    int ans=0;

    while(q.sz>1){
        int a=q.top();q.pop();
        int b=q.top();q.pop();
        ans+=a+b;
        q.push(a+b);
    }
    cout<<ans;
}

洛谷 - P1168 中位数

  • 如果直接用排序加取中位数的方法,时间复杂度很高,因此可以想到对顶堆来做。
void solve() {
    cin>>n;
    priority_queue<int> q1;
    priority_queue<int, vector<int>, greater<int>> q2;

    FOR(i,1,n){
        int x;
        cin>>x;
        q1.push(x);
        while(q1.sz>q2.sz){
            q2.push(q1.top());
            q1.pop();
        }
        while(q2.sz>q1.sz){
            q1.push(q2.top());
            q2.pop();
        }
        if(i&1){
            cout<<q1.top()<<endl;
        }
    }
}

洛谷 - P2085 最小函数值

  • 就是搞一个堆,然后把函数的值都放进去。这边存在一个优化,就是这边先把 $x=1$ 带入每个函数里面去,然后把这些得到的值都放入堆中,然后从堆中取出较小的值,然后把 $x+1$ 的值又放入堆中,这样是最优的。
struct E{
    int y,x,id;
    bool operator<(const E& t)const{
        return y>t.y;
    }
};

void solve() {
    cin>>n>>m;

    priority_queue<E>q;

    FOR(i,1,n){
        cin>>a[i]>>b[i]>>c[i];
        q.push({a[i]+b[i]+c[i],1,i});
    }

    FOR(i,1,m){
        E t=q.top();
        q.pop();
        cout<<t.y<<" ";
        q.push({a[t.id]*(t.x+1)*(t.x+1)+b[t.id]*(t.x+1)+c[t.id],t.x+1,t.id});
    }

}

洛谷 - P2827 蚯蚓

  • 这道题如果每次切断都去维护一下堆的话,时间复杂度特别高,因此对于堆,如果要优化,一般想到的都是队列。首先我们明白一个不等式 $x_1-\lfloor px_1 \rfloor \ge x_2 \lfloor px_2 \rfloor$,具体证明过程请 看,包含所有细节。根据这个不等式,我们可以考虑维护三个队列,而不必维护一个优先队列,因为三个队列始终保持满足单调性,无需使用优先队列。然后根据此思路即可拿到所有 $q = 0$ 的分,接下来为了解决 $q \gt 0$,通常方法都是计个 $tag$,加入队列时把两段都减掉一个 $q$,$tag$ 加上一个 $q$。考虑到一条在第 $i$ 时刻加入的长度为 $len$ 蚯蚓,如果在第 $j$ 时刻被拿出来砍,因为它一共经历了 $j - i - 1$ 次切割,一次切割长度加 $q$,所以总长度是 $q \times (j - i - 1) + len$。可以在常数时间内算出,不必打 $tag$ 标记,只需多存个加入时间。
int n,m,q,u,v,t,w[N];
queue<int>qw[4];
// int INF=1e18;

void solve() {
    cin>>n>>m>>q>>u>>v>>t;

    FOR(i,1,n)cin>>w[i];

    sort(w+1,w+1+n,GI());

    FOR(i,1,n){
        qw[1].push(w[i]);
    }

    FOR(i,0,m-1){
        PII tt=max({
            make_pair(qw[1].sz?qw[1].front():INF,1),
            make_pair(qw[2].sz?qw[2].front():INF,2),
            make_pair(qw[3].sz?qw[3].front():INF,3)
        });
        int x=tt.fi+q*i,y=tt.se;
        qw[y].pop();

        if(i%t==t-1){
            cout<<x<<" ";
        }

        int b=x*u/v,c=x-b;
        qw[2].push(b-q-q*i);
        qw[3].push(c-q-q*i);
    }

    cout<<endl;

    FOR(i,1,n+m){
        PII tt=max({
            make_pair(qw[1].sz?qw[1].front():INF,1),
            make_pair(qw[2].sz?qw[2].front():INF,2),
            make_pair(qw[3].sz?qw[3].front():INF,3)
        });
        int x=tt.fi,y=tt.se;
        qw[y].pop();

        if(i%t==0){
            cout<<x+q*m<<" ";
        }
    }

}

洛谷 - P3045 Cow Coupons G

  • 反悔贪心的题目。

反悔贪心是什么?

答:其实贪心本身不带有反悔,是因为此时的贪心可以从局部最优解推出全局最优解。但当有些时候局部最优解推不出全局最优解时,就要用反悔贪心,在适当的时候撤销之前做出的决策。

  • 我们先选取 $c$ 最小的 $k$ 个物品使用优惠劵,当前已经使用的价格是 $tot$。下文为方便表述,记使用优惠劵的物品集合为 $A$,他在求解过程中不是固定不变的。
    当前考虑第 $i$ 个物品,由于 $k$ 张优惠券已经用完了,所以只能以原价 $p_i$ 购买物品 $i$。现在考虑反不反悔的条件是什么。
    如果要反悔,那么用优惠券买 $i$ 的价格一定要小于用原价买 $i$。当 $i$ 用了优惠券,那么 $A$ 势必要有一个物品(记为 $j,j \in A$)做出退让,用原价来买 $j$。(其实相当于用 $i$ 来代替 $j$),那么一定满足以下不等式:

$$tot-c_j+p_j+c_i\lt tot+p_i$$

意思是:$i$ 代替 $j$ 用优惠券的价格比用原价买 $i$ 便宜,这个时候就需要反悔。
发现 $tot$ 可以消去:
$$-c_j+p_j+c_i\lt p_i$$

然后把下标相同的归在小于号的同一侧:
$$p_j-c_j\lt p_i-c_i$$
他们的形式是相同的,所以我们可以设 $\Delta_i=p_i-c_i$:
$$\Delta_j\lt\Delta_i$$
所以只要在已经使用优惠券的物品里面,存在一个 $j$,使得 $\Delta_j\lt\Delta_i$,我们就需要用 $i$ 代替 $j$ 使用优惠券。也就是 $k$ 个物品中,$(\Delta_j)_{\min}\lt\Delta_i$ 。注意这个不是恒等式,因为 $i \notin A$,但是 $j\in A$。
最小值可以用优先队列来求。

引用

int n,k,m;
int p[N],c[N];
priority_queue<PII,vector<PII>,GII>P,C;
priority_queue<int,VI,GI>delta;
bool vis[N];

void solve() {
    cin>>n>>k>>m;

    FOR(i,1,n){
        cin>>p[i]>>c[i];
        P.push({p[i],i});
        C.push({c[i],i});
    }

    FOR(i,1,k)delta.push(0);

    int ans=0;

    while(P.sz){
        PII x=P.top();
        PII y=C.top();

        if(vis[x.se]){
            P.pop();
            continue;
        }
        if(vis[y.se]){
            C.pop();
            continue;
        }
        //原价买比按优惠价买并且补上差价便宜的话,我们就就用原价买
        if(delta.top()+y.fi>x.fi){//用原价买 i 更划算
            m-=x.fi;
            vis[x.se]=1;
            P.pop();
        }else{//用优惠券买 i 更划算
            m-=y.fi+delta.top();
            vis[y.se]=1;
            C.pop();
            delta.pop();
            delta.push(p[y.se]-c[y.se]);
        }

        if(m<0)break;
        ans++;
    }
    cout<<ans;
}

待深入总结

posted @ 2025-03-13 00:10  shuying6  阅读(73)  评论(0)    收藏  举报