数据结构与算法:dp优化——优化尝试和状态设计

前言

神奇妙妙dp!

一、741. 摘樱桃

cpp 复制代码
class Solution {
public:
    int cherryPickup(vector<vector<int>>& grid) {
        int n=grid.size();

        //贪心的方法不行!
        //对于这种一来一回的问题,往往可以转化成两起点问题
        //e.g.一个人一来一回 -> 两个人同步从起点开始走一趟
        //"同步"使得不存在有一个格子两个人先后到达

        vector<vector<vector<int>>>dp(n,vector<vector<int>>(n,vector<int>(n,-2)));

        int ans=dfs(0,0,0,dp,n,grid);
        return ans==-1?0:ans;
    }

    //当前A人来到(a,b),B人来到(c,d)
    //由于是同步走的,所以a+b==c+d,所以d=a+b-c
    int dfs(int a,int b,int c,
    vector<vector<vector<int>>>&dp,int n,vector<vector<int>>&grid)
    {
        int d=a+b-c;

        //道路不存在
        if(a==n||b==n||c==n||d==n||grid[a][b]==-1||grid[c][d]==-1)
        {
            return -1;
        }
        //因为同步走,所以若A到了,那么B也到了
        if(a==n-1&&b==n-1)
        {
            return grid[a][b];
        }
        if(dp[a][b][c]!=-2)
        {
            return dp[a][b][c];
        }

        //当前格子的收益
        int cur=(a==c&&b==d)?grid[a][b]:(grid[a][b]+grid[c][d]);
        
        //往后走的收益
        int next=dfs(a+1,b,c+1,dp,n,grid);
        next=max(next,dfs(a+1,b,c,dp,n,grid));
        next=max(next,dfs(a,b+1,c+1,dp,n,grid));
        next=max(next,dfs(a,b+1,c,dp,n,grid));

        int ans=-1;
        if(next!=-1)
        {
            ans=next+cur;
        }

        dp[a][b][c]=ans;
        return ans;
    }
};

上来的第一道题就提供了一个很牛逼的trick,就是对于这种一来一回问题,可以考虑将其转化成两个人同步从起点出发走一趟。注意,这里"同步"的设置使得不存在一个格子两人先后到达。

之后,考虑定义dp[a][b][c]为当前两人分别位于(a,b)和(c,d)位置,这里因为两人是同步走的,所以走过的曼哈顿距离是一样的,所以第二个人的d是可以通过其他三个坐标计算得到的。之后就是计算一下当前格子的收益,若两人位于同一格子,那么就只能得到一份收益,否则就是两个格子的累加和。然后考虑四种往后的可能性,若有效就计算最大收益然后记忆化即可。

二、青蛙过河

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 INF 1e9
#define INFLL 1e18
#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 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};

void solve()
{
    ll n,x;
    cin>>n>>x;
    vector<ll>a(n+1);
    for(int i=1;i<n;i++)
    {
        cin>>a[i];
    }
    //补一个
    a[n]=2*x;

    //一共跳2x次 -> 有2x个青蛙要跳到对岸
    //因为存在单调性:若步长step可以达到,那么大于step的步长一定都可以达到
    //所以考虑使用二分答案
    
    //最初,若1~step的石头的累加和大于等于2x,说明1~step的石头可以承载所有青蛙
    //之后,若2~step+1的石头累加和大于等于2*x,说明1位置上的青蛙可以跳到2~step+1范围内
    //那么对于当前的步长step,考虑设置一个长度为step的滑动窗口
    //若窗口内的累加和大于等于2x,说明当前step长度的石头可以承载所有青蛙

    //进一步考虑,可以使用双指针
    //每次从当前位置l开始往后找最小的r,使得窗口内累加和大于等于2*x
    //那么整个过程中窗口的最大值就是所需的步长

    auto check=[&](int mid)->bool
    {
        ll sum=0;
        for(int i=1;i<=mid;i++)
        {
            sum+=a[i];
        }

        for(int l=1,r=mid+1;l<=n;l++,r++)
        {
            if(sum<2*x)
            {
                return false;
            }

            sum-=a[l];
            if(r<=n)
            {
                sum+=a[r];
            }
        }

        return true;
    };

    int l=1;
    int r=n;
    int m;
    int ans=0;
    while(l<=r)
    {
        m=l+r>>1;
        if(check(m))
        {
            ans=m;
            r=m-1;
        }
        else
        {
            l=m+1;
        }
    }
    cout<<ans<<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;
}

还是这个trick,对于来回跳2x次的问题,可以转化为有2x个青蛙要跳到对岸。之后,因为存在单调性:若步长step可以达到,那么大于step的步长一定都可以达到,所以考虑使用二分答案。

最初,若1~step的石头的累加和大于等于2x,说明1~step的石头可以承载所有青蛙。之后,若2~step+1的石头累加和大于等于2x,说明1位置上的青蛙可以跳到2~step+1范围内。那么对于当前的步长step,考虑设置一个长度为step的滑动窗口。若窗口内的累加和大于等于2x,说明当前step长度的石头可以承载所有青蛙。

进一步考虑,其实是可以使用双指针优化的。方法就是每次从当前位置l开始往后找最小的r,使得窗口内累加和大于等于2x,那么整个过程中窗口的最大值就是所需的步长。这样就把二分答案的O(logn)也优化掉了。

三、B. The Number of Products

经典cf思维题()

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 INF 1e9
#define INFLL 1e18ll
#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 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};

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

    //将正数看作0,负数看作1,那么相乘就可以看作异或运算
    //所以定义dp[i][0]为子数组以i位置结尾,乘积为正的个数,dp[i][1]为乘积为负的个数
    //所以考虑维护前缀异或,那么dp[i][0]就是前缀中pre[j]^pre[i]=0的个数

    ll ans1=0,ans0=0;
    int zero=1,one=0;
    int pre=0;
    for(int i=1;i<=n;i++)
    {
        pre^=(a[i]<0);

        if(pre)
        {
            ans1+=one;
            ans0+=zero;

            one++;
        }
        else
        {
            ans1+=zero;
            ans0+=one;

            zero++;
        }
    }

    cout<<ans0<<" "<<ans1<<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;
}

还是一个trick,对于正负问题,考虑将正数看作0,负数看作1,那么相乘就可以看作异或运算。

所以还是这个常见的状态设计,即定义dp[i][0]为子数组以i位置结尾,乘积为正的个数,dp[i][1]为乘积为负的个数。之后,考虑维护前缀异或和,那么dp[i][0]就是前缀中pre[j]^pre[i]=0的个数。那么这个就可以通过几个变量递推了,注意初始什么都没有时可以看作乘以1,所以zero要设置为1。

四、绝世好题

这个状态设计太逆天了,确实是绝世好题......

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 INF 1e9
#define INFLL 1e18ll
#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 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};

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

    //定义pre[j]为
    //之前的数组成的所有合法的子序列中,最后的数第j位是1的最长的子序列长度
    //那么对于当前数是1的位,就可以把之前最长的子序列变得更长

    vector<int>pre(32);
    for(int i=1;i<=n;i++)
    {
        int x=a[i];

        //自己
        int cur=1;
        for(int j=0;j<31;j++)
        {
            if((x>>j)&1)
            {
                cur=max(cur,pre[j]+1);
            }
        }

        for(int j=0;j<31;j++)
        {
            if((x>>j)&1)
            {
                pre[j]=max(pre[j],cur);
            }
        }
    }

    int ans=0;
    for(int j=0;j<31;j++)
    {
        ans=max(ans,pre[j]);
    }
    cout<<ans<<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;
}

这个题的思想感觉比较像最长递增子序列,都是用一个结构维护可能性。

首先,题目的要求是,对于生成的子序列,相邻两数至少有一位需要都是1。所以,可以考虑定义pre[j]为,之前的数组成的所有合法的子序列中,最后的数第j位是1的最长的子序列长度。那么对于当前数是1的位,就可以让当前数参与把之前最长的子序列变得更长。那么就可以每次取最大值,找出当前数参与作为子序列最后一位时,能构成的最长子序列长度。然后,同样对于当前数是1的位,就可以去看是否能把pre更新得更长。最后,答案就是pre中每一位是1时的最长子序列长度中的最大值。

五、摆盘子的方法

一共有n个盘子,k种菜,所有盘子排成一排,每个盘子只能放一种菜,要求最多连续两个盘子菜品一样,不允许更长的重复出现,返回摆盘的方案数,对1e9+7取模。1<=n<=1000,1<=k<=1000。

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 INF 1e9
#define INFLL 1e18ll
#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 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 MOD=1e9+7;

ll dfs(int i,int k,vector<int>&path)
{
    if(i==path.size())
    {
        int len=1;
        for(int j=1;j<path.size();j++)
        {
            if(path[j-1]==path[j])
            {
                len++;
            }
            else
            {
                len=1;
            }

            if(len>2)
            {
                return 0;
            }
        }
        return len>2?0:1;
    }

    int ans=0;
    for(int j=0;j<k;j++)
    {
        path[i]=j;
        ans+=dfs(i+1,k,path);
    }
    return ans;
}

//暴力解
ll solve1(int n,int k)
{
    vector<int>path(n);
    return dfs(0,k,path);
}

//dp解
ll solve2(int n,int k)
{
    if(n==1)
    {
        return k;
    }

    //定义dp[i]为当前来到位置i,且i和i+1位置菜品不一样的合法方案数
    //如果要让i和i-1位置的菜一样,那么依赖的就是dp[i-2]*(k-1)
    //如果要让i和i-1位置的菜不一样,那么依赖的就是dp[i-1]*(k-1)
    //最后来到n位置时
    //如果要让n-1和n位置的菜不一样,那么对于n位置选的每一种菜,都可以获得一份dp[n-1],所以依赖的就是dp[n-1]*k
    //如果要让n-1和n位置的菜一样,同样对于选的每一种菜,都可以获得一份dp[n-2],所以依赖的就是dp[n-2]*k

    vector<ll>dp(n+1);

    dp[0]=1;
    dp[1]=k-1;

    for(int i=2;i<n;i++)
    {
        dp[i]=((dp[i-1]+dp[i-2])%MOD*(k-1))%MOD;
    }

    return (dp[n-1]+dp[n-2])%MOD*k%MOD;
}

void test()
{
    srand(time(0));

    cout<<"S"<<endl;

    int testTime=100;
    for(int i=1;i<=testTime;i++)
    {
        int n=rand()%8+1;
        int k=rand()%10+1;

        ll ans1=solve1(n,k);
        ll ans2=solve2(n,k);

        if(ans1!=ans2)
        {
            cout<<"W"<<endl;
        }

        if(i%10==0)
        {
            cout<<"Have tested "<<i<<" cases"<<endl;
        }
    }
    cout<<"E"<<endl;
}

void init()
{
}

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

这个题的状态设计也挺妙的。

考虑定义dp[i]为当前来到位置i,且i和i+1位置菜品不一样的合法方案数。

那么如果要让i和i-1位置的菜一样,那么依赖的就是dp[i-2]*(k-1)。此时因为根据定义,dp[i-2]要求了i-2和i-1位置的菜品不一样,所以可以直接和后续接上。而如果要让i和i-1位置的菜不一样,那么依赖的就是dp[i-1]*(k-1)。

此外,还要注意边界的特判。就是最后来到n位置时,如果要让n-1和n位置的菜不一样,此时因为对于n位置选的每一种菜,都可以获得一份dp[n-1],所以依赖的就是dp[n-1]*k,而不是和之前一样的dp[n-1]*(k-1)。如果要让n-1和n位置的菜一样,同样对于选的每一种菜,都可以获得一份dp[n-2],所以依赖的就是dp[n-2]*k。

六、过河

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 INF 1e9
#define INFLL 1e18ll
#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 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};

void solve()
{
    int l;
    cin>>l;
    int s,t,m;
    cin>>s>>t>>m;
    vector<int>a(m+1);
    for(int i=1;i<=m;i++)
    {
        cin>>a[i];
    }

    //定义dp[i]为当前来到i位置,经过的最少石子数
    //每次依赖之前[s,t]距离的位置,那么若i位置是石头,答案就需要+1

    //注意到l很大的,直接这样dp肯定会tle和mle
    //又注意到石子的数量是很少的,所以整个路径上石子的分布是很稀疏的
    //又因为[s,t]也很小,所以当相邻两块石头相隔很远时
    //dp值在中间很长一段距离是不会变的,所以可以考虑进行缩点
    //缩点时可以直接乱搞,反正[s,t]很小,随便设一个安全距离即可 

    sort(a.begin()+1,a.end());

    //每次只能跳固定距离
    if(s==t)
    {
        int ans=0;
        for(int i=1;i<=m;i++)
        {
            if(a[i]%s==0)
            {
                ans++;
            }
        }
        cout<<ans<<endl;
        return ;
    }

    int safe=666;

    //对石子重定位
    vector<int>where(m+1);
    vector<int>stone(safe*m+1);
    for(int i=1;i<=m;i++)
    {
        where[i]=where[i-1]+min(a[i]-a[i-1],safe);
        stone[where[i]]=1;
    }

    l=min(l,where[m]+safe);

    vector<int>dp(l+1,INF);

    dp[0]=0;

    for(int i=1;i<=l;i++)
    {
        for(int j=max(i-t,0);j<=i-s;j++)
        {
            dp[i]=min(dp[i],dp[j]+stone[i]);
        }
    }

    int ans=INF;
    for(int i=where[m]+1;i<=l;i++)
    {
        ans=min(ans,dp[i]);
    }
    cout<<ans<<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;
}

这个题其实就涉及到了 缩点 这个比较重要的思想了。

这个题的状态设计还是挺简单的,就是定义dp[i]为当前来到i位置,经过的最少石子数。那么每次依赖之前[s,t]距离的位置dp值的最小值,然后若i位置是石头,答案就需要+1。

但是,注意到桥的长度L很大的,直接这样定义dp肯定会tle和mle。但是又注意到石子的数量是很少的,所以可以想到,整个路径上石子的分布是很稀疏的。又因为[s,t]也很小,所以当相邻两块石头相隔很远时,dp值在中间很长一段距离是不会变的,所以可以考虑进行缩点。

之后,首先特判掉s等于t,即每次只能跳固定距离的情况。然后,在这个题里,缩点时可以直接乱搞,反正[s,t]很小,随便设一个安全距离safe即可。之后就是根据这个安全距离对石子进行重定位。若两颗石子的间距很大时,就认为其间距为safe。所以整个桥的长度L就可以被减小到safe*m,此时就可以正常跑dp了。

现在才发现,其实转移时的枚举行为可以用单调队列优化的。

七、放苹果

经典计数题,不得不说还是左神讲的好。之前遇到这个题的时候去oiwiki上看得头晕脑胀的,现在学左神一讲就懂,左神伟大!!

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=10+5;

vector<vector<ll>>dp(MAXN,vector<ll>(MAXN,-1));

//dfs(m,n):当前还剩m个苹果,n个盘子,允许有空盘的方法数
ll dfs(int m,int n)
{
    //没苹果了 -> 找到一种
    if(m==0)
    {
        return 1;
    }
    //没盘子了 -> 无效方案
    if(n==0)
    {
        return 0;
    }
    if(dp[m][n]!=-1)
    {
        return dp[m][n];
    }

    int ans;
    
    //盘子数大于苹果树 -> 必然有n-m个空盘
    if(n>m)
    {
        ans=dfs(m,m);
    }
    else
    {
        //dfs(m,n-1):不用所有的盘子 -> 来一个空盘
        //dfs(m-n,n):必须用所有的盘子 -> 给所有盘子分配一个
        ans=dfs(m,n-1)+dfs(m-n,n);
    }

    dp[m][n]=ans;
    return ans;
}

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

    cout<<dfs(m,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;
}

这个题的状态定义还好,就是定义dp[m][n]为当前还剩m个苹果,要分到n个盘子里,允许有空盘的方法数。那么首先,若m等于0了,即没苹果了,那么就说明找到了一种。否则,若n等于0了,那就说明苹果没分完,就说明是无效方案。

之后,若n大于m,即盘子数大于苹果数,那么必然会导致空盘的出现。此时就是和把m个苹果放到m个盘子里是等效的,所以直接依赖dp[m][m]。否则,即苹果数大于等于盘子数,那么就可以分两种情况讨论。第一种情况就是可以不用所有盘子,那么就可以先来一个空盘,依赖的就是dp[m][n-1]。第二种情况就是必须用所有的盘子,那么此时就可以先给每个盘子都分配一个苹果,之后再考虑往每个盘子里额外放几个苹果,所以依赖dp[m-n][n]。

八、数的划分

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};

void solve1()
{
    int n,k;
    cin>>n>>k;

    //模板:将i个苹果放进j个盒子里,不允许空盒
    //首先必须保证每个盒子非空,所以可以考虑先往j个盒子里各放一个
    //那么之后就转化成了将i-j个苹果放进j个盒子,此时怎么分配都是合理的了
    //所以转移dp[i][j]=sum(p=1~j)dp[i-j][p]
    //注意到dp[i-1][j-1]=sum(p=1~j-1)dp[i-j][p]
    //因为i和j同减1时i-j不变,但依赖的少了dp[i-j][j]
    //所以可以优化成dp[i][j]=dp[i-1][j-1]+dp[i-j][j]

    //dp[i][j]:把i分成j个数的方案数
    vector<vector<ll>>dp(n+1,vector<ll>(k+1));

    dp[0][0]=1;

    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=min(k,i);j++)
        {
            dp[i][j]=dp[i-1][j-1]+dp[i-j][j];
        }
    }

    cout<<dp[n][k]<<endl;
}

const int MAXN=200+5;
const int MAXK=6+5;

vector<vector<ll>>dp(MAXN,vector<ll>(MAXK,-1));

ll dfs(int n,int k)
{
    if(n==0)
    {
        return 1;
    }
    if(k==0)
    {
        return 0;
    }
    if(dp[n][k]!=-1)
    {
        return dp[n][k];
    }

    int ans;
    if(k>n)
    {
        ans=dfs(n,n);
    }
    else
    {
        ans=dfs(n,k-1)+dfs(n-k,k);
    }

    dp[n][k]=ans;
    return ans;
}

void solve2()
{
    int n,k;
    cin>>n>>k;

    if(n<k)
    {
        cout<<0<<endl;
        return ;
    }

    if(n==k)
    {
        cout<<1<<endl;
        return ;
    }

    //先给每个盘子都分配一个
    n-=k;

    cout<<dfs(n,k)<<endl;
}

void init()
{
}

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

这就是经典的分拆数问题。

这个问题其实和上个问题把m个苹果放进n个盘子是类似的,唯一不同的就是要求每个盘子不能为空。那么其实可以考虑上来先给每个盘子都分配一个,那么之后就是把m-n个苹果放进n个盘子,允许有空盘了的问题了。那么上来先给n减去k,然后去跑上个题的dfs即可。

再说说以前自己学的时候的思路,定义dp[i][j]为把i个苹果放进j个盘子,不允许有空盘的方案数。首先,根据dp的定义,有转移方程dp[i][j]等于dp[i-j][1~j]的累加和。这是因为把当前i个苹果放进j个盘子,不允许有空盘的方案数,就等于先往j个盘子里放一个苹果,那么还剩下i-j个苹果。之后,就等价于把这些剩下的苹果放进1~j个盘子的方案数之和。进一步观察可以发现,dp[i-1][j-1]等于dp[i-j][1~j-1]的累加和,那么只需要把少的dp[i-j][j]加上即可。

感觉还是左神的思路好懂!

九、最好的部署

一共有n台机器,编号1~n,所有机器排成一排。你只能一台一台地部署机器,你可以决定部署的顺序,最终所有机器都要部署。

给定三个数组no,one和both。其中no[i]表示如果i号机器部署时,相邻没有机器部署,能获得的收益。one[i]表示如果i号机器部署时,相邻有一台机器部署,能获得的收益。both[i]表示如果i号机器部署时,相邻有两台机器部署,能获得的收益。认为第1号和第n号机器不会有两台相邻的机器,返回部署的最大收益。其中1<=n<=1e5,no[i],one[i]和both[i]均为正数。

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;

int n;

vector<int>no(MAXN);
vector<int>one(MAXN);
vector<int>both(MAXN);

//部署[l,r]范围上的机器
//且l-1和r+1的机器都没部署的最大收益
int dfs(int l,int r,vector<vector<int>>&dp)
{
    if(l==r)
    {
        return no[l];
    }
    if(dp[l][r]!=-1)
    {
        return dp[l][r];
    }

    //由于每台机器既依赖左侧又依赖右侧
    //所以考虑"时光倒流"的思想,讨论每台机器最后部署的情况
    
    //第l个机器最后部署 和 第r个机器最后部署
    int ans=max(dfs(l+1,r,dp)+one[l],dfs(l,r-1,dp)+one[r]);
    //枚举区间内哪台机器最后部署
    for(int p=l+1;p<=r-1;p++)
    {
        ans=max(ans,dfs(l,p-1,dp)+dfs(p+1,r,dp)+both[p]);
    }

    dp[l][r]=ans;
    return ans;
}

//区间dp暴力方法 -> O(n^3)
int solve1()
{
    vector<vector<int>>dp(n+1,vector<int>(n+1,-1));

    return dfs(1,n,dp);
}

//线性dp正解
int solve2()
{
    //dp[i][p]:当前来到第i号机器,p为0表示前一台没部署,p为1表示前一台部署了
    vector<vector<int>>dp(n+1,vector<int>(2));
    
    dp[n][0]=no[n];
    dp[n][1]=one[n];

    for(int i=n-1;i>=1;i--)
    {
        dp[i][0]=max(dp[i+1][1]+no[i],dp[i+1][0]+one[i]);
        dp[i][1]=max(dp[i+1][1]+one[i],dp[i+1][0]+both[i]);
    }

    return dp[1][0];
}

void randomArray()
{
    for(int i=1;i<=n;i++)
    {
        no[i]=rand()%2000;
        one[i]=rand()%2000;
        both[i]=rand()%2000;
    }
}

void test()
{
    srand(time(0));

    n=rand()%100+1;

    randomArray();

    int ans1=solve1();
    int ans2=solve2();

    if(ans1!=ans2)
    {
        cout<<"W"<<endl;
        cout<<n<<endl;
        for(int i=1;i<=n;i++)
        {
            cout<<no[i]<<" "<<one[i]<<" "<<both[i]<<endl;
        }
    }
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    cin>>t;
    init();
    cout<<"S"<<endl;
    while(t--)
    {
        test();   
    }
    cout<<"E"<<endl;
    return 0;
}

状态机dp(!

考虑定义dp[i][0]为当前来到第i号机器,前一台机器没部署,dp[i][1]为前一台机器部署了。此时,因为前一台机器的状态已经确定了,那么就可以从后往前dp,考虑后一台机器的状态了。所以,初始化时dp[n][0]就是no[n],dp[n][1]就是one[n]。之后,从后往前dp,那么对于dp[i][0],如果先部署当前机器后部署右侧机器,那么就是dp[i+1][1]再加上one[i]。而若先部署右侧机器再部署当前机器,那么就是dp[i+1][0]再加上one[i]。类似地,dp[i][1]依赖的就是dp[i+1][0]+both[i]和dp[i+1][1]+one[i]。

其实区间dp的暴力解也不好想,考虑定义dp[l][r]为部署[l,r]范围上的机器,且l-1和r+1的机器都没部署的最大收益。那么当只剩一个机器时,收益就是no[l]。之后,由于每个机器既依赖左侧又依赖右侧,那么就可以考虑"时光倒流"的方法,讨论每台机器最后部署的情况。所以特判一下端点位置,然后暴力枚举区间内哪个位置最后部署即可。

十、增加限制的最长公共子序列

给定两个字符串s和t,s长度为n,t长度为m,返回s和t的最长公共子序列长度。其中两字符串都由小写字母组成,1<=n<=1e6,1<=m<=1e3。

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=1e6+5;
const int MAXM=1e3+5;

int n,m;

string s,t;

//经典最长公共子序列dp ->O(n*m)
int solve1()
{
    vector<vector<int>>dp(n+1,vector<int>(m+1));
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            if(s[i]==t[j])
            {
                dp[i][j]=1+dp[i-1][j-1];
            }
            else
            {
                dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
            }
        }
    }
    return dp[n][m];
}

//当前每个字符最早出现的位置
vector<int>suf(26);

//从当前位置开始,每个字符下一次出现的位置
vector<vector<int>>nextt(MAXN,vector<int>(26));

vector<vector<int>>dp(MAXM,vector<int>(MAXM));

//对较长的字符串s建立查询结构
void build()
{
    for(int j=0;j<26;j++)
    {
        suf[j]=INF;
    }

    for(int i=n;i>=0;i--)
    {
        for(int j=0;j<26;j++)
        {
            nextt[i][j]=suf[j];
        }

        if(i>0)
        {
            suf[s[i]-'a']=i;
        }
    }

    for(int i=0;i<=m;i++)
    {
        for(int j=0;j<=m;j++)
        {
            dp[i][j]=-1;
        }
    }
}

//长度为i的t的前缀串,想和s串形成长度为j的公共子序列
//至少需要s串多长的前缀串才能做到
int dfs(int i,int j)
{   
    //做不到
    if(i<j)
    {
        return INF;
    }
    if(j==0)
    {
        return 0;
    }
    if(dp[i][j]!=-1)
    {
        return dp[i][j];
    }

    int cur=t[i]-'a';

    //不要t串中i位置的字符
    int ans=dfs(i-1,j);

    //必须要t串中i位置的字符
    int pre=dfs(i-1,j-1);
    if(pre!=INF)
    {
        //从之前达到的位置pre开始,往后cur字符第一次出现的位置
        ans=min(ans,nextt[pre][cur]);
    }

    dp[i][j]=ans;
    return ans;
}

//正解 -> O(m^2)
int solve2()
{
    build();

    //t串整体能和s串形成多长的子序列
    for(int j=m;j>=1;j--)
    {
        if(dfs(m,j)!=INF)
        {
            return j;
        }
    }
    return 0;
}

void randomString()
{
    s=" ",t=" ";
    for(int i=1;i<=n;i++)
    {
        s+=char(rand()%26+'a');
    }
    for(int i=1;i<=m;i++)
    {
        t+=char(rand()%26+'a');
    }
}

void test()
{
    srand(time(0));

    n=rand()%1000+1;
    m=rand()%1000+1;
    randomString();

    int ans1=solve1();
    int ans2=solve2();

    if(ans1!=ans2)
    {
        cout<<"W"<<endl;
        cout<<n<<endl;
        cout<<s<<endl;
        cout<<m<<endl;
        cout<<t<<endl;
        cout<<ans1<<" "<<ans2<<endl;
    }
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    cin>>t;
    cout<<"S"<<endl;
    for(int i=1;i<=t;i++)
    {
        test();

        if(i%100==0)
        {
            cout<<"Have tested "<<i<<" cases"<<endl;
        }
    }
    cout<<"E"<<endl;
    return 0;
}

逆天状态设计......

回顾经典的最长公共子序列问题,可以发现复杂度是O(n*m),肯定不行,需要想新的解。

考虑定义dp[i][j]为长度为i的t的前缀串,想和s串形成长度为j的公共子序列,至少需要s串多长的前缀串。首先,如果i小于j,即t的前缀串的长度比子序列长度j小,那么肯定不可能达成,返回无穷大。否则若j等于0长度,那么0长度就可以。之后,分别讨论不要t串i位置的字符和一定要这个字符两个情况。那么如果不要的话就是dp[i-1][j],而如果要的话,若dp[i-1][j-1]有可能达成,那么最短需要的长度,就是从前面达成的长度dp[i-1][j-1]开始往后,去找当前字符第一次出现的位置,这个就是需要达成的最短长度。所以求解时只需要从大往小枚举j,那么第一个能达成的j就是答案。

之后,就需要考虑去构建一个next表,可以快速查从i位置开始往后第一个j字符出现的位置了。那么就可以考虑设置一个suf表存每个字符上一次出现的位置,然后每次先往next表里刷一遍suf,然后更新suf表中当前字符的位置即可。

所以,预处理的复杂度为O(26*n),在dp的过程中由于i小于j时就直接返回了,又因为i是t串的长度,所以复杂度其实是O(m^2)的。

十一、鸡蛋掉落

太变态了这个题......

cpp 复制代码
class Solution {
public:
    int superEggDrop(int k, int n) {
    
        return solve1(k,n);
        //return solve2(k,n);
    }

    //左神方法
    int solve1(int k,int n)
    {
        //dp[i][j]:有i个鸡蛋,只能扔j次
        //最多能解决多少层楼的这个问题
        vector<vector<int>>dp(k+1,vector<int>(n+1));

        //只能扔1次时,不管有多少个鸡蛋,都只能解决一层楼的问题
        //只有1个鸡蛋时,只能从下往上一层一层试,所以能解决j层楼的问题

        for(int j=1;j<=n;j++)
        {
            for(int i=1;i<=k;i++)
            {
                //对于当前有i个鸡蛋,能扔j次的情况
                //当选择了第x层楼扔
                //若鸡蛋没碎,那么说明f必然在x层上方,那么还有i个鸡蛋,能扔j-1次
                //若鸡蛋碎了,那么说明f必然在x层下方,那么还有i-1个鸡蛋,能扔j-1次
                //所以此时最大可以解决的层数为dp[i][j-1]+dp[i-1][j-1]+1
                dp[i][j]=dp[i-1][j-1]+dp[i][j-1]+1;

                //从小到大枚举j时,第一次遇到dp[i][j]>=n能完成时就直接返回
                if(dp[i][j]>=n)
                {
                    return j;
                }
            }
        }
        return -1;
    }

    //自己写的暴力解 -> O(n*k*logn)
    int solve2(int k,int n)
    {
        vector<vector<int>>dp(k+1,vector<int>(n+1,-1));

        int ans=dfs(k,n,dp);
        return ans;
    }

    //dfs(k,n):当前还剩k个鸡蛋,要测n层楼的最小操作数
    int dfs(int k,int n,vector<vector<int>>&dp)
    {
        if(n==0)
        {
            return 0;
        }
        if(k==1)
        {
            return n;
        }
        if(dp[k][n]!=-1)
        {
            return dp[k][n];
        }

        int l=1;
        int r=n;
        int m;
        int ans;
        while(l<=r)
        {
            m=(l+r)>>1;

            //没碎 -> 去更高的楼层
            int p1=dfs(k,n-m,dp);
            //碎了 -> 去下一层
            int p2=dfs(k-1,m-1,dp);

            //选最大的
            if(p1<=p2)
            {
                ans=m;
                r=m-1;
            }
            if(p1>=p2)
            {
                ans=m;
                l=m+1;
            }
        }

        dp[k][n]=max(dfs(k,n-ans,dp),dfs(k-1,ans-1,dp))+1;
        return dp[k][n];
    }
};

先说左神方法,考虑定义 dp[i][j] 为有 i 个鸡蛋,只能扔 j 次,最多能解决多少层楼的这个问题。首先,只能扔1次时,不管有多少个鸡蛋,都只能解决一层楼的问题。而只有1个鸡蛋时,只能从下往上一层一层试,所以能解决 j 层楼的问题。

之后,对于当前有 i 个鸡蛋,能扔 j 次的情况,当选择了第 x 层楼扔,若鸡蛋没碎,那么说明 f 必然在 x 层上方,那么还有 i 个鸡蛋,能扔 j-1 次。而若鸡蛋碎了,那么说明 f 必然在 x 层下方,那么还有 i-1 个鸡蛋,能扔 j-1 次。所以此时最大可以解决的层数为 dp[i][j-1]+dp[i-1][j-1]+1。那么在从小到大枚举 j 时,第一次遇到 dp[i][j] 大于等于 n,即能完成时就可以直接返回了。

再说说我自己的方法,考虑定义dp[k][n]为当前还剩 k 个鸡蛋,要测 n 层楼的最小操作数。那么若 n 等于0了答案就是0,若 k 等于1只剩一个鸡蛋了答案就是n。之后,由于存在单调性,即楼层数越大扔的次数越多。又因为是要求最大值最小,所以每次考虑二分在 m 层楼扔,每次考虑碎了和没碎两种情况。若碎了的操作数更大,那么就去更低的楼层二分,否则就去更高的楼层二分。最后带着结果再跑一次取最大值即可。

十二、E - Average and Median

不亏是abc,教育意义是真的强!

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};

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

    //dp[i][0]:当前来到i位置,可以要也可以不要的子序列最大累加和
    //dp[i][1]:当前来到i位置,必须要的子序列最大累加和
    vector<vector<double>>dp1(n+1,vector<double>(2));

    vector<double>help1(n+1);

    auto DP1=[&]()->double
    {
        dp1[0][0]=dp1[1][0]=0;
        for(int i=1;i<=n;i++)
        {
            dp1[i][0]=max(help1[i]+dp1[i-1][0],dp1[i-1][1]);
            dp1[i][1]=help1[i]+dp1[i-1][0];
        }
        return max(dp1[n][0],dp1[n][1]);
    };

    //能否选择一个子序列,使得最大平均值大于等于x
    auto check1=[&](double x)->bool
    {
        for(int i=1;i<=n;i++)
        {
            help1[i]=(double)a[i]-x;
        }

        return DP1()>=0;
    };

    //找最大平均值
    //若最小值为l,最大值为r,那么平均值必然在这个范围内
    //假设平均值为m,考虑让所有数字都减去m
    //若子序列最大累加和大于等于0,就说明最大平均值至少为m
    auto average=[&]()->double
    {
        double l=0;
        double r=INF;
        for(int i=1;i<=n;i++)
        {
            l=min(l,(double)a[i]);
            r=max(r,(double)a[i]);
        }   

        double eps=1e-5;
        double m;
        double ans;
        while(l+eps<=r)
        {
            m=(l+r)/2;
            if(check1(m))
            {
                ans=m;
                l=m;
            }
            else
            {
                r=m;
            }
        }
        return ans;
    };

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

    sort(sorted.begin()+1,sorted.end());

    vector<vector<int>>dp2(n+1,vector<int>(2));

    vector<int>help2(n+1);

    auto DP2=[&]()->int
    {
        dp2[0][0]=dp2[1][0]=0;
        for(int i=1;i<=n;i++)
        {
            dp2[i][0]=max(help2[i]+dp2[i-1][0],dp2[i-1][1]);
            dp2[i][1]=help2[i]+dp2[i-1][0];
        }
        return max(dp2[n][0],dp2[n][1]);
    };

    //能否选择一个子序列,使得大于等于x的个数在一半以上
    auto check2=[&](int x)->bool
    {
        for(int i=1;i<=n;i++)
        {
            help2[i]=a[i]>=x?1:-1;
        }

        return DP2()>0;
    };


    //找最大中位数
    //假设平均值为m,考虑让大于等于m的数为+1,小于的数为-1
    //若子序列最大累加和大于0,说明最大中位数至少为m
    auto median=[&]()->int
    {
        int l=0;
        int r=n-1;
        int m;
        int ans=-1;
        while(l<=r)
        {
            m=l+r>>1;
            if(check2(sorted[m]))
            {
                ans=sorted[m];
                l=m+1;
            }
            else
            {
                r=m-1;
            }
        }
        return ans;
    };

    cout<<average()<<endl;
    cout<<median()<<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;
}

首先,考虑最大平均值的情况。那么若最小值为 l,最大值为 r,那么平均值必然在这个范围内。这里有一个很常见的trick,假设平均值为 m,考虑让所有数字都减去 m。若子序列最大累加和大于等于0,就说明最大平均值至少为m。所以就可以考虑去二分答案,每次先把填 help 数组,然后去跑子序列 check。check 就是考虑定义dp[i][0]为当前来到 i 位置,可以要也可以不要的子序列最大累加和,dp[i][1]为当前来到 i 位置,必须要的子序列最大累加和。那么最后判断一下子序列最大累加和,然后继续二分即可。

对于最大中位数的情况,还是一个常见的 trick,假设平均值为 m,考虑让大于等于 m 的数为+1,小于的数为-1。那么若子序列最大累加和大于0,说明子序列最大中位数大于等于为 m。那么就还是考虑去二分答案,每次 check 时还是先填help数组,然后去子序列 dp 即可。

十三、将珠子放入背包中

cpp 复制代码
class Solution {
public:

    typedef long long ll;

    long long putMarbles(vector<int>& weights, int k) {
        int n=weights.size();

        //分析每种划分方案最终价值的构成
        //首先可以发现不管怎么划分,开头和结尾的价值一定会被算入
        //之后,产生贡献的珠子,一定位于划分点的两侧
        //存在n-1个可选择的划分点,统计每个划分点两侧珠子的贡献并排序
        //那么最小的方案就是选最小的k-1个划分点,最大的方案就是选最大的k-1个划分点

        vector<ll>vals;
        for(int i=0;i<n-1;i++)
        {
            vals.push_back(weights[i]+weights[i+1]);
        }

        sort(vals.begin(),vals.end());

        ll minn=0;
        for(int i=0;i<k-1;i++)
        {
            minn+=vals[i];
        }

        ll maxx=0;
        for(int i=vals.size()-1,j=0;j<k-1;i--,j++)
        {
            maxx+=vals[i];
        }

        return maxx-minn;
    }
};

这个题倒是感觉还好(?

考虑分析每种划分方案最终价值的构成,首先可以发现不管怎么划分,开头和结尾的价值一定会被算入。之后可以发现,题意就是说产生贡献的珠子一定位于划分点的两侧。划分为 k 个区间就相当于在 n-1 个可选择的划分点中选 k-1 个,那么就可以考虑统计每个划分点两侧珠子的贡献并排序。那么最小的方案就是选最小的 k-1 个划分点,最大的方案就是选最大的 k-1 个划分点。

总结

还是多见题多写题多积累,加油!

END

相关推荐
im_AMBER2 小时前
Leetcode 105 K 个一组翻转链表
数据结构·学习·算法·leetcode·链表
sin_hielo2 小时前
leetcode 1877
数据结构·算法·leetcode
睡不醒的kun2 小时前
定长滑动窗口-基础篇(2)
数据结构·c++·算法·leetcode·职场和发展·滑动窗口·定长滑动窗口
小王努力学编程3 小时前
LangChain——AI应用开发框架(核心组件1)
linux·服务器·前端·数据库·c++·人工智能·langchain
庄小焱3 小时前
【机器学习】——房屋销售价格预测实战
人工智能·算法·机器学习·预测模型
txzrxz3 小时前
单调栈详解(含题目)
数据结构·c++·算法·前缀和·单调栈
AI科技星3 小时前
张祥前统一场论的数学表述与概念梳理:从几何公设到统一场方程
人工智能·线性代数·算法·机器学习·矩阵·数据挖掘
程序员-King.3 小时前
day167—递归—二叉树的直径(LeetCode-543)
算法·leetcode·深度优先·递归
亲爱的非洲野猪3 小时前
2动态规划进阶:背包问题详解与实战
算法·动态规划·代理模式