数据机构与算法:dp优化——倍增优化

前言

倍增真的很强!!

一、最多可以参加的会议数目 II

cpp 复制代码
class Solution {
public:
    int maxValue(vector<vector<int>>& events, int k) {
        int n=events.size();

        //考虑对会议按结束时间从小到大排序
        //定义dp[i][j]为前i个会议中最多参加j个的最大收益
        //对于每个会议,可以选择参加或不参加

        //如果要参加的话,可以从前面结束时间小于当前开始时间的会议转移过来
        //又因为存在单调性:dp[i][j]<=dp[i+1][j]
        //所以可以二分找结束时间小于当前开始时间的最右位置

        sort(events.begin(),events.end(),[&](vector<int>&x,vector<int>&y)
        {
            return x[1]<y[1];
        });

        vector<vector<int>>dp(n,vector<int>(k+1));

        for(int j=1;j<=k;j++)
        {
            dp[0][j]=events[0][2];
        }

        for(int i=1;i<n;i++)
        {
            int pre=bs(events[i][0],i-1,events);

            for(int j=1;j<=k;j++)
            {
                dp[i][j]=max(dp[i-1][j],events[i][2]+(pre==-1?0:dp[pre][j-1]));
            }
        }
        return dp[n-1][k];
    }

    int bs(int v,int r,vector<vector<int>>&a)
    {
        int l=0;
        int m;
        int ans=-1;
        while(l<=r)
        {
            m=l+r>>1;

            if(a[m][1]<v)
            {
                ans=m;
                l=m+1;
            }
            else
            {
                r=m-1;
            }
        }
        return ans;
    }
};

其实这个题和倍增没啥关系()

对于这种开会这种区间问题,还是上来就考虑按左端点或右端点排序。那么在这个题里,就考虑对会议按结束时间从小到大排序,然后定义 dp[i][j] 为前 i 个会议中最多参加 j 个的最大收益。因为对于每个会议,可以选择参加或不参加,所以如果要参加的话,可以从前面结束时间小于当前开始时间的会议转移过来。又因为存在单调性:dp[i][j]<=dp[i+1][j],所以可以二分找结束时间小于当前开始时间的最右位置。

二、跑路

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

const int MAXN=50+5;
const int MAXP=64;

//stjump[i][j][p]:从i到j是否有一条长度为2的p次方的路径
bool stjump[MAXN][MAXN][MAXP+1];
//dis[i][j]:从i到j最少需要几秒
int dis[MAXN][MAXN];

int n,m;

void build()
{
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=n;j++)
        {
            stjump[i][j][0]=0;
            dis[i][j]=INF;
        }
    }
}

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

    build();

    for(int i=1,u,v;i<=m;i++)
    {
        cin>>u>>v;

        stjump[u][v][0]=1;
        dis[u][v]=1;
    }

    //倍增
    for(int p=1;p<=MAXP;p++)
    {
        //Floyd -> 枚举跳点
        for(int jump=1;jump<=n;jump++)
        {
            for(int i=1;i<=n;i++)
            {
                for(int j=1;j<=n;j++)
                {
                    if(stjump[i][jump][p-1]&&stjump[jump][j][p-1])
                    {
                        stjump[i][j][p]=1;
                        dis[i][j]=1;
                    }
                }
            }
        }
    }

    //Floyd
    for(int jump=1;jump<=n;jump++)
    {
        for(int i=1;i<=n;i++)
        {
            for(int j=1;j<=n;j++)
            {
                if(dis[i][jump]!=INF&&dis[jump][j]!=INF)
                {
                    dis[i][j]=min(dis[i][j],dis[i][jump]+dis[jump][j]);
                }
            }
        }
    }

    cout<<dis[1][n]<<endl;
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

没想到 floyd 这玩意儿的复杂度居然真能用上()

因为这个折跃门每次是跳2的p次方步的,那么其实很自然就能想到用倍增去维护。首先,可以考虑求出在一秒内从每个点能到哪些点,那么这个就可以直接倍增。首先,考虑定义 stjump[u][v][p] 为从 u 到 v 是否存在一条长度为 2 的 p 次方的路径,再定义 dis[u][v] 表示从 u 到 v 最少需要花几秒。那么对于每一条边 (u,v),必然会带来一条长度为 2 的 0 次方的边,那么就设置 st 表并设置 dis[u][v] 为 1。之后,考虑倍增更新 st 表和 dis 表。此时在枚举每个 p 时,因为点的个数很少,所以这里就可以考虑使用 floyd 算法。那么就是三重循环枚举,每次若从 i 到跳点和跳点到 j 都存在一条长度为 2 的 p-1 次方长度的路径,那么 i 到 j 就存在一条长度为 2 的 p 次方的路径,同时更新 dis[i][j]。最后再跑一遍 floyd 算法,更新 dis 即可。

注意,虽然点的个数很小,但因为每次是跳 2 的 p 次方,那么其实是可以通过在环上绕非常多圈从而一步跳到想去的位置的,所以最终路径长度也可能会很长,所以需要把 MAXP 开大点。

三、统计重复个数

cpp 复制代码
class Solution {
public:

    typedef long long ll;

    int getMaxRepetitions(string s1, int a, string s2, int b) {
        int n=s1.length();
        int m=s2.length();

        //next[i][j]:从s1的i位置出发,到下一个j字符的距离
        vector<vector<int>>next(n,vector<int>(26));

        //s2中存在某个字符在s1中没出现过
        if(!find(s1,n,s2,next))
        {
            return 0;
        }

        //st[i][p]:从s1的i位置出发,走多远能获得2^p个s2
        vector<vector<ll>>st(n,vector<ll>(30+1));

        //构建st[i][0]
        for(int i=0;i<n;i++)
        {
            int cur=i;
            ll len=0;
            for(auto c:s2)
            {
                len+=next[cur][c-'a'];
                cur=(cur+next[cur][c-'a'])%n;
            }

            st[i][0]=len;
        }

        //倍增
        for(int p=1;p<=30;p++)
        {
            for(int i=0;i<n;i++)
            {
                st[i][p]=st[i][p-1]+st[(i+st[i][p-1])%n][p-1];
            }
        }

        //a个s1中能拼出几个s2
        ll ans=0;
        for(int p=30,cur=0;p>=0;p--)
        {
            //从当前位置cur开始,是否能拼出2^p个s2
            if(cur+st[cur%n][p]<=n*a)
            {
                cur+=st[cur%n][p];
                ans+=1ll<<p;
            }
        }

        //用a个s1能拼出ans个s2,s2拼接了b次
        //所以需要再拼接ans/b次即可
        return ans/b;
    }

    bool find(string &s1,int n,string &s2,vector<vector<int>>&next)
    {
        vector<int>right(26,-1);

        for(int i=n-1;i>=0;i--)
        {
            right[s1[i]-'a']=i+n;
        }

        for(int i=n-1;i>=0;i--)
        {
            right[s1[i]-'a']=i;
            for(int j=0;j<26;j++)
            {
                if(right[j]!=-1)
                {
                    next[i][j]=right[j]-i+1;
                }
                else
                {
                    next[i][j]=-1;
                }
            }
        }

        for(auto c:s2)
        {
            //s1中不存在s2中的这个字符
            if(next[0][c-'a']==-1)
            {
                return false;
            }
        }
        return true;
    }
};

首先,因为 n1,n2 都很大,那么复杂度基本上肯定不能和这两个量有关了。那么在拼接的过程中,可以考虑用倍增处理能拼出几个的问题。那么就可以考虑定义 st[i][p] 为从s1 的 i 位置出发,走多远能获得 2 的 p 次方个 s2。那么首先,st[i][0] 的含义是从 i 位置出发走多远能获得 1 个s2。那么在拼 s2 的过程中,由于每次都要找到从当前位置开始下一个某字符的位置,所以考虑通过预处理一个结构解决。那么考虑定义 next[i][j] 为从 s1 的 i 位置开始,到下一个 j 字符的距离。这个只需要维护一个 right 表示上一个 j 字符出现的位置,那么每次往 next 里刷即可。那么首先就可以进行特判,若 s1 中不存在 s2 的某个字符,那么就必然不可能拼出来。

之后,在构建 st[i][0] 拼 s2 的过程中,可以每次用 next 数组往下跳,每次取个模即可。那么有了 st[i][0],就可以开始倍增了。最后只需要考虑 a 个 s1 能拼出几个 s2,那么直接通过倍增往下跳,最后把能拼出的个数除以 b 就是能拼出几个重复 b 次的 s2。

四、双链表解决最近与次近问题

在看下个题之前,先看以下问题:

给定一个长度为 n 的数组 arr,下标 1~n,数组内无重复值。关于近和距离的定义如下:对 i 位置的数字 x 来说,只关注右侧的数字,和 x 的差值的绝对值越小就越近,距离就是差值的绝对值。如果距离一样,数值越小的越近。求每个数字最近和次近的位置及其距离,如果不存在用 0 表示。

cpp 复制代码
const int MAXN=1e5+5;
const int LIMIT=20;

vector<int>h(MAXN);

int n;

//从i出发,第一近和第二近的点及距离
vector<int>to1(MAXN);
vector<int>dis1(MAXN);
vector<int>to2(MAXN);
vector<int>dis2(MAXN);

bool cmp(const pii &x,const pii &y)
{
    return x.second!=y.second?x.second<y.second:x.first<y.first;
} 

void update(int i,int j)
{
    if(j==0)
    {
        return ;
    }

    int dis=abs(h[i]-h[j]);

    if(to1[i]==0||dis<dis1[i]||(dis==dis1[i]&&h[j]<h[to1[i]]))
    {
        to2[i]=to1[i];
        dis2[i]=dis1[i];

        to1[i]=j;
        dis1[i]=dis;
    }
    else if(to2[i]==0||dis<dis2[i]||(dis==dis2[i]&&h[j]<h[to2[i]]))
    {
        to2[i]=j;
        dis2[i]=dis;
    }
}

//有序表方法
void near1()
{
    set<pii,decltype(&cmp)>st(cmp);

    //倒序构建
    for(int i=n;i>=1;i--)
    {
        pii cur={i,h[i]};
        
        //小于cur的两条
        auto p1=st.lower_bound(cur);
        if(p1!=st.begin())
        {
            p1=prev(p1);
            update(i,p1->first);

            if(p1!=st.begin())
            {
                auto p2=prev(p1);
                update(i,p2->first);
            }
        }
        
        //大于cur的两条
        auto p3=st.upper_bound(cur);
        if(p3!=st.end())
        {
            update(i,p3->first);

            auto p4=next(p3);
            if(p4!=st.end())
            {
                update(i,p4->first);
            }
        }

        st.insert(cur);
    }
}

vector<array<int,2>>sorted(MAXN);
vector<int>pre(MAXN);
vector<int>nxt(MAXN);

void remove(int i)
{
    int l=pre[i];
    int r=nxt[i];

    if(l!=0)
    {
        nxt[l]=r;
    }
    if(r!=0)
    {
        pre[r]=l;
    }
}

//双向链表方法
void near2()
{
    for(int i=1;i<=n;i++)
    {
        sorted[i][0]=i;
        sorted[i][1]=h[i];
    }

    sort(sorted.begin()+1,sorted.begin()+n+1,[&](auto &x,auto &y)
    {
        return x[1]<y[1];
    });

    sorted[0][0]=0;
    sorted[n+1][0]=0;

    //排名
    for(int i=1;i<=n;i++)
    {
        pre[sorted[i][0]]=sorted[i-1][0];
        nxt[sorted[i][0]]=sorted[i+1][0];
    }

    //原数组下标
    for(int i=1;i<=n;i++)
    {
        update(i,pre[i]);
        update(i,pre[pre[i]]);
        update(i,nxt[i]);
        update(i,nxt[nxt[i]]);

        remove(i);
    }
}

首先说有序表方法,那么就是定义一个 set,根据数值从小到大排序。注意这里写比较器时要考虑数值相同的情况,否则 set 会只保留一个键。之后从后往前考虑,每次查小于当前数值的两条和大于当前数值的两条,比较看能否更新最近和次近,然后把当前值和下标加入 set 即可。

再说双向链表的实现,考虑先把所有值拿出来从小到大排序,然后根据排名去生成双向链表。最后只需要从左到右考察整个数组,通过这个双向链表每次考察左侧的两个位置和右侧的两个位置,此时就是右侧大于自己和小于自己的两条数据,最后删除自己这个节点,防止干扰后续即可。

五、开车旅行

解决了上面这个问题,就可以正式开始这个题了(晕)

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

const int MAXN=1e5+5;
const int LIMIT=20;

vector<int>h(MAXN);

int n;

//从i出发,第一近和第二近的点及距离
vector<int>to1(MAXN);
vector<int>dis1(MAXN);
vector<int>to2(MAXN);
vector<int>dis2(MAXN);

bool cmp(const pii &x,const pii &y)
{
    return x.second!=y.second?x.second<y.second:x.first<y.first;
} 

void update(int i,int j)
{
    if(j==0)
    {
        return ;
    }

    int dis=abs(h[i]-h[j]);

    if(to1[i]==0||dis<dis1[i]||(dis==dis1[i]&&h[j]<h[to1[i]]))
    {
        to2[i]=to1[i];
        dis2[i]=dis1[i];

        to1[i]=j;
        dis1[i]=dis;
    }
    else if(to2[i]==0||dis<dis2[i]||(dis==dis2[i]&&h[j]<h[to2[i]]))
    {
        to2[i]=j;
        dis2[i]=dis;
    }
}

//有序表方法
void near1()
{
    set<pii,decltype(&cmp)>st(cmp);

    //倒序构建
    for(int i=n;i>=1;i--)
    {
        pii cur={i,h[i]};
        
        //小于cur的两条
        auto p1=st.lower_bound(cur);
        if(p1!=st.begin())
        {
            p1=prev(p1);
            update(i,p1->first);

            if(p1!=st.begin())
            {
                auto p2=prev(p1);
                update(i,p2->first);
            }
        }
        
        //大于cur的两条
        auto p3=st.upper_bound(cur);
        if(p3!=st.end())
        {
            update(i,p3->first);

            auto p4=next(p3);
            if(p4!=st.end())
            {
                update(i,p4->first);
            }
        }

        st.insert(cur);
    }
}

vector<array<int,2>>sorted(MAXN);
vector<int>pre(MAXN);
vector<int>nxt(MAXN);

void remove(int i)
{
    int l=pre[i];
    int r=nxt[i];

    if(l!=0)
    {
        nxt[l]=r;
    }
    if(r!=0)
    {
        pre[r]=l;
    }
}

//双向链表方法
void near2()
{
    for(int i=1;i<=n;i++)
    {
        sorted[i][0]=i;
        sorted[i][1]=h[i];
    }

    sort(sorted.begin()+1,sorted.begin()+n+1,[&](auto &x,auto &y)
    {
        return x[1]<y[1];
    });

    sorted[0][0]=0;
    sorted[n+1][0]=0;

    //排名
    for(int i=1;i<=n;i++)
    {
        pre[sorted[i][0]]=sorted[i-1][0];
        nxt[sorted[i][0]]=sorted[i+1][0];
    }

    //原数组下标
    for(int i=1;i<=n;i++)
    {
        update(i,pre[i]);
        update(i,pre[pre[i]]);
        update(i,nxt[i]);
        update(i,nxt[nxt[i]]);

        remove(i);
    }
}

//a开一次b开一次称为一轮

//stto[i][p]:从i位置出发,经过2^p轮到了什么位置
vector<vector<int>>stto(MAXN,vector<int>(LIMIT));
//stdis[i][p]:从i位置出发,经过2^p轮走了多远
vector<vector<int>>stdis(MAXN,vector<int>(LIMIT));
//sta[i][p]:从i位置出发,经过2^p轮,a走了多远
vector<vector<int>>sta(MAXN,vector<int>(LIMIT));
//stb[i][p]:从i位置出发,经过2^p轮,b走了多远
vector<vector<int>>stb(MAXN,vector<int>(LIMIT));

void build()
{
    for(int i=1;i<=n;i++)
    {
        stto[i][0]=to1[to2[i]];
        stdis[i][0]=dis2[i]+dis1[to2[i]];
        sta[i][0]=dis2[i];
        stb[i][0]=dis1[to2[i]];
    }

    for(int p=1;p<LIMIT;p++)
    {
        for(int i=1;i<=n;i++)
        {
            stto[i][p]=stto[stto[i][p-1]][p-1];
            
            //没开出去
            if(stto[i][p]!=0)
            {
                stdis[i][p]=stdis[i][p-1]+stdis[stto[i][p-1]][p-1];
                sta[i][p]=sta[i][p-1]+sta[stto[i][p-1]][p-1];
                stb[i][p]=stb[i][p-1]+stb[stto[i][p-1]][p-1];
            }
        }
    }
}

int a,b;

void go(int s,int x)
{
    a=0,b=0;

    for(int p=LIMIT-1;p>=0;p--)
    {
        if(stto[s][p]!=0&&x>=stdis[s][p])
        {
            x-=stdis[s][p];
            a+=sta[s][p];
            b+=stb[s][p];
            s=stto[s][p];
        }
    }

    //a还能多开一次
    if(dis2[s]<=x)
    {
        a+=dis2[s];
    }
}

int best(int x0)
{
    int ans=0;
    double rat=INF;
    //从每个点开始跑一遍
    for(int i=1;i<n;i++)
    {
        go(i,x0);

        double cur=INF;

        if(b!=0)
        {
            cur=(double)a/b;
        }

        if(ans==0||cur<rat||(cur==rat&&h[i]>h[ans]))
        {
            rat=cur;
            ans=i;
        }
    }
    return ans;
}

void solve()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>h[i];
    }

    near2();
    build();

    int x0;
    cin>>x0;
    cout<<best(x0)<<endl;

    int m;
    cin>>m;
    int s,x;
    while(m--)
    {
        cin>>s>>x;

        go(s,x);

        cout<<a<<" "<<b<<endl;
    }
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

首先,定义 a 开一次 b 开一次为一轮,之后考虑构建四张倍增表,分别是:stto[i][p] 表示从 i 出发经过 2 的 p 次方轮能到达哪个点,stdis[i][p] 表示从 i 位置出发,经过 2 的 p 次方轮走了多远,sta[i][p] 表示从 i 位置出发,经过 2 的 p 次方轮,a 走了多远,stb[i][p] 表示从 i 位置出发,经过 2 的 p 次方轮,b 走了多远。那么因为有了最近和次近的两张表,这四个表的构建就非常简单了。

构建完倍增表后,每次查询时,只需要根据这个倍增表往后跳,每次记录 a 和 b 分别开了多远即可,注意最后需要特判 a 能否单独开一次。那么在有了这个方法后,只需要枚举从每个位置开始开,每次查询一下取最小值即可。

总结

还是太难......

END

相关推荐
恒者走天下2 小时前
操作系统内核项目面经分享
c++
YYYing.2 小时前
【Linux/C++进阶篇(二) 】超详解自动化构建 —— 日常开发中的“脚本” :Makefile/CMake
linux·c++·经验分享·ubuntu
范纹杉想快点毕业2 小时前
嵌入式实时系统架构设计:基于STM32与Zynq的中断、状态机与FIFO架构工程实战指南,基于Kimi设计
c语言·c++·单片机·嵌入式硬件·算法·架构·mfc
玖釉-2 小时前
核心解构:Cluster LOD 与 DAG 架构深度剖析
c++·windows·架构·图形渲染
lovod2 小时前
【视觉SLAM十四讲】建图
算法·视觉slam
程序员敲代码吗2 小时前
C++运行库修复指南:解决游戏办公软件报错问题
开发语言·c++·游戏
SmartBrain2 小时前
AI算法工程师面试:大模型和智能体知识(含答案)
人工智能·算法·语言模型·架构·aigc
孞㐑¥2 小时前
算法—哈希表
开发语言·c++·经验分享·笔记·算法
近津薪荼2 小时前
递归专题(2)——合并链表
c++·学习·算法·链表