牛客小白月赛34

本文介绍了一种优化最长不下降子序列问题的方法,通过状态压缩dp和贪心策略,将时间复杂度从O(n^2)降低。主要涉及动态规划的改进,如使用q数组存储长度子序列的最小值,以及如何利用二分查找加速状态转移。

A dd爱科学1.0

主要思路

本质上求最长不下降子序列,dp解法,详细版本可以参考y总算法基础课

f[i]表示以a[i]结尾的最长不下降子序列长度,考虑如何更新f[i]

用j遍历1到i - 1,当w[i] >= w[j]时,更新f[i] = max(f[i], f[j] + 1)

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;
int f[N], n;
char a[N];

int main(void)
{
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> a[i];
    
    for(int i = 1; i <= n; i++)
    {
        f[i] = 1;//f[i]最小值为1
        for(int j = 1; j < i; j++)
        {
            if(a[i] >= a[j]) f[i] = max(f[i], f[j] + 1);//更新f[i]
        }
    }
    
    int res = 0;
    for(int i = 1; i <= n; i++) res = max(res, f[i]);
    
    cout << n - res << endl;
    return 0;
}

复杂度为O(n ^ 2),本题的数据范围为10 ^ 6会超时,考虑如何优化

首先考虑每一种长度的不下降子序列我们只需要存储该子序列末尾的最小值即可。

假设以两个元素a[i]和a[j]结尾的最长不下降子序列长度都为len,且a[i] < a[j],那么我们后续在更新其他元素时一定会用a[i]更新,不会用a[j]更新,所以每一种长度的不下降子序列我们只需要存储该子序列末尾的最小值即可。

我们用q[i]存储长度为i的最长不下降子序列结尾的最小值。

继续考虑q[i]数组的性质。

用反证法,假设q[i] > q[i + 1],也就是说长度为i的子序列末尾的最小值 > 长度为i + 1子序列末尾的最小值,那么

我们一定可以用q[i + 1]去更新q[i],所以一定有q[i] <= q[i + 1],所以q[i]数组具有单调性。

我们在更新f[i]时,只需要二分出q数组中第一个值小于等于a[i]的即可,也就是说找到值小于等于a[i]的最长长度。

#include <iostream>
#include <cstring>
#include <string>
#include <algorithm>
#include <string>
using namespace std;

#define int long long
#define debug(a) cout << #a << " = " << a << endl;
#define x first
#define y second
typedef pair<int, int> P;

const int N = 1000010;
int n, f[N], q[N];

signed main(void)
{
    std::ios::sync_with_stdio(false);
    cin >> n;
    string t;
    cin >> t;
    int len = 0;
    for(int i = 0; i < n; i++)
    {
        int l = 0, r = len;//二分
        while(l < r)
        {
            int mid = l + r + 1 >> 1;
            if(q[mid] <= t[i]) l = mid;
            else r = mid - 1;
        }
        len = max(len, r + 1);//二分的结果r就是值小于等于t[i]的最长长度,更新最长长度
        q[r + 1] = t[i];//更新当前r+1长度的值。
        //因为如果q[r+1]是小于等于t[i]的那么二分的结果应该为r+1,不是r
        //所以q[r+1]一定大于t[i],直接赋值即可
    }
    cout << n - len << endl;
    return 0;
}

B dd爱探险

主要思路

状态压缩dp

首先 n个点 n-1次跳完,明白这个跳跃过程其实是一条链,每个点只有被经过和没被经过两个状态,所以典型状压dp。

对于一个数i,我们把它转化为2进制,2进制一共n位,第i位为0表示第i个点没被经过,为1表示第i个点已经被经过

定义f [ i ] [ j ] [k]为当前经过点的状态为i,当前在j这个点上,k为0表示没经历过加速,k为1表示仅经历过重力加速,k为2表示仅经历过反重力加速,k为3表示经历过两次加速。

枚举最后停留的位置j,j范围为1~n,答案为min(f[(1 << n) - 1] [j] [3])。1 << n - 1表示所有点都被经过,有n个1的状态。

状态转移方程见代码

#include <iostream>
#include <cstring>
#include <string>
#include <algorithm>
#include <stack>
#include <string>
#include <cmath>
#include <unordered_map>
#include <queue>
using namespace std;

#define debug(a) cout << #a << " = " << a << endl;
#define x first
#define y second
typedef pair<int, int> P;

const int N = 17;
int n, p[N][N];
int f[1 << N][N][4], st[N];
int a[N], b[N];

int main(void)
{
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= n; j++)
        {
            cin >> p[i][j];
        }
    }
    
    memset(f, 0x3f, sizeof(f));
    
    for(int i = 1; i <= n; i++)
    {
        f[1 << (i - 1)][i][0] = 0;
    }
    
    for(int t = 1; t < (1 << n); t++)
    {
        int x = t;
        for(int i = 1; i <= n; i++) st[i] = x & 1, x >>= 1;//更新当前状态上第i个点有没有被访问过
        
        int ta = 0, tb = 0;
        for(int i = 1; i <= n; i++)
        {
            if(st[i]) a[++ta] = i;//被访问过的存到a数组
            else b[++tb] = i;//没被访问过的存到b数组
        }
        
        for(int i = 1; i <= ta; i++)//枚举由哪个已经被经过的点跳到哪个没被经过的点
        {
            for(int j = 1; j <= tb; j++)
            {
                //t + (1 << (b[j] - 1))表示将b[j]位修改为1
                f[t + (1 << (b[j] - 1))][b[j]][0] = min(f[t + (1 << (b[j] - 1))][b[j]][0], f[t][a[i]][0] + p[a[i]][b[j]]);
                //没经历过加速的点只能由没经历过加速的点转移
                f[t + (1 << (b[j] - 1))][b[j]][1] = min(f[t + (1 << (b[j] - 1))][b[j]][1], min(f[t][a[i]][1] + p[a[i]][b[j]], f[t][a[i]][0]));
                //经过重力加速的点能由没经历过加速的点和经历过重力加速的点转移
                f[t + (1 << (b[j] - 1))][b[j]][2] = min(f[t + (1 << (b[j] - 1))][b[j]][2], min(f[t][a[i]][2] + p[a[i]][b[j]], f[t][a[i]][0] + 2 * p[a[i]][b[j]]));
                //经历过反重力加速的点能由没经历过加速的点和经历过反重力加速的点转移
                f[t + (1 << (b[j] - 1))][b[j]][3] = min(f[t + (1 << (b[j] - 1))][b[j]][3], min(f[t][a[i]][3] + p[a[i]][b[j]], min(f[t][a[i]][1] + 2 * p[a[i]][b[j]], f[t][a[i]][2])));
                //经历过两次加速的点能由经历过一次加速的点和经历过两次加速的点转移
            }
        }
    }
    int res = 0x3f3f3f3f;
    for(int i = 1; i <= n; i++)
    {
        res = min(res, f[(1 << n) - 1][i][3]);//更新答案
    }
    cout << res << endl;
    return 0;
}

C dd爱科学2.0

f[i] [j]表示更改到第i个位置 更新成的字母为 j 的总偏移量和

f[i] [j]为min(f[i - 1] [j]), j从1开始,换句话说f[i] [j]由第i - 1位中小于等于j的更新而来

#include <iostream>
#include <cstring>
#include <string>
#include <algorithm>
#include <stack>
#include <string>
#include <cmath>
#include <unordered_map>
#include <queue>
using namespace std;

#define int long long
#define debug(a) cout << #a << " = " << a << endl;
#define x first
#define y second
typedef pair<int, int> P;

const int N = 1000010;
int n, f[N][30];

signed main(void)
{
    std::ios::sync_with_stdio(false);
    cin >> n;
    string t;
    cin >> t;
    
    for(int i = 0; i < 26; i++) f[0][i] = abs(t[0] - 'A' - i);
    for(int i = 1; i < n; i++)
    {
        int minf = 0x3f3f3f3f;//维护f[i - 1][j]中更新成小于等于j的字母中的最小值
        for(int j = 0; j < 26; j++)
        {
            minf = min(minf, f[i - 1][j]);
            f[i][j] = minf + abs(t[i] - 'A' - j);
        }
    }
    
    int res = 0x3f3f3f3f;
    for(int i = 0; i < 26; i++)
    {
        res = min(res, f[n - 1][i]);//遍历结尾更新成哪个字母结果最小
    }
    cout << res << endl;
    return 0;
}

第二种dp方式

f[i] [j]表示改至第i位位置,最后一位小于等于j的最小值

则f[i] [j] = min(f[i] [j - 1], f[i - 1] [j] + abs(t[i] - ‘A’ - j));

状态转移方程的含义是f[i] [j]可以由改至第i位位置,最后一位小于等于j - 1的状态改至第i - 1位位置,最后一位小于等于j的状态更新

最后答案位f[n] [26]

#include<bits/stdc++.h>
using namespace std;
int n,ans;
char a[1000005];
int f[1000005][30];
int main(){
	scanf("%d",&n);
	scanf("%s",a+1);
	memset(f,0x3f,sizeof(f));
	for (int i=1;i<=26;++i)f[0][i]=0;
	for (int i=1;i<=n;++i)
		for (int j=1;j<=26;++j)f[i][j]=min(f[i][j-1],f[i-1][j]+abs(a[i]-(j-1+'A')));
	printf("%d",f[n][26]);
} 

D dd爱矩阵

贪心+优先队列

我们先把问题简化,如果两行,每行 n个数,怎么选和前n大的所有数

把两行分别降序 sort,如果令第一行为数组 a,第二行为数组 b

则可得到最大值为 a[1]+b[1]

那么如何快速求出最大的n个数呢

我们把a[1] + b[j]的和, 1 <= j <= n,和当前a的位置,a[1]记为1,放入一个结构体中存入优先队列中,每次取出和最大的一个数,然后找到a的位置,让a的位置+1的数来替代当前a,同时把更新过的和放入优先队列(因为更新过后下一个前n大的数一定在优先队列中),取n次最大值就能找到前n大的所有数

这是两行的操作,我们进行n-1次就能求出所有前n大的数

复杂度n^2logn

#include <iostream>
#include <cstring>
#include <string>
#include <algorithm>
#include <stack>
#include <string>
#include <cmath>
#include <unordered_map>
#include <queue>
using namespace std;

#define int long long
#define debug(a) cout << #a << " = " << a << endl;
#define x first
#define y second
typedef pair<int, int> P;

const int N = 1000010;
int n;
int a[N], b[N], c[N];

struct node
{
    int pos, w;
    friend bool operator < (node x,node y) {
        return x.w < y.w;
    }
};

bool cmp(int a, int b)
{
    return a > b;
}

signed main(void)
{
    priority_queue<node> q;
    cin >> n;
    for(int i = 0; i < n; i++) cin >> a[i];
    sort(a, a + n, cmp);
    
    for(int i = 2; i <= n; i++)
    {
        for(int j = 0; j < n; j++)
        {
            cin >> b[j];
            q.push({0, a[0] + b[j]});//把a数组的位置和a[0],b数组的和放入优先队列
        }
        
        for(int j = 0; j < n; j++)
        {
            node t = q.top();//取出队头和最大的元素
            q.pop();
            c[j] = t.w;//记录一下前n大的元素
            int pos = t.pos + 1;//当前位置以及a数组的下一个位置
            q.push({pos, t.w - a[t.pos] + a[pos]});
        }
        
        for(int j = 0; j < n; j++) a[j] = c[j];
    }
    for(int i = 0; i < n; i++) cout << c[i] << ' ';
    return 0;
}

E dd爱旋转

操作1, 2进行两次都等同于没进行操作,模拟即可

#include <iostream>
#include <cstring>
#include <string>
#include <algorithm>
#include <stack>
#include <string>
#include <cmath>
#include <unordered_map>
#include <queue>
using namespace std;

#define int long long
#define debug(a) cout << #a << " = " << a << endl;
#define x first
#define y second
typedef pair<int, int> P;

const int N = 1010;
int n, q;
int a[N][N];
int ans[N][N];

void print()
{
    for(int i = 0; i < n; i++)
    {
        for(int j = 0; j < n; j++)
        {
            cout << ans[i][j] << ' ';
        }
        cout << endl;
    }
}

signed main(void)
{
    std::ios::sync_with_stdio(false);
    cin >> n;
    int t1 = 0, t2 = 0;
    for(int i = 0; i < n; i++)
    {
        for(int j = 0; j < n; j++)
        {
            cin >> a[i][j];
            ans[i][j] = a[i][j];
        }
    }
    
    cin >> q;
    for(int i = 0; i < q; i++)
    {
        int x;
        cin >> x;
        if(x == 1) t1++;
        else t2++;
    }
    t1 %= 2;
    t2 %= 2;
    if(t1)
    {
        int cnt1 = 0, cnt2 = 0;
        for(int i = n - 1; i >= 0; i--)
        {
            for(int j = n - 1; j >= 0; j--)
            {
                ans[cnt1][cnt2++] = a[i][j];
            }
            cnt1++;
            cnt2 = 0;
        }
        memcpy(a, ans, sizeof(ans));
    }
    
    if(t2)
    {
        int cnt1 = 0, cnt2 = 0;
        for(int i = n - 1; i >= 0; i--)
        {
            for(int j = 0; j < n; j++)
            {
                ans[cnt1][cnt2++] = a[i][j];
            }
            cnt1++;
            cnt2 = 0;
        }
    }
    
    print();
    return 0;
}

F dd爱框框

思路1

前缀和+二分

首先二分出最小的区间长度,然后用前缀和遍历即可

#include <iostream>
#include <cstring>
#include <string>
#include <algorithm>
#include <stack>
#include <string>
#include <cmath>
#include <unordered_map>
#include <queue>
using namespace std;

#define int long long
#define debug(a) cout << #a << " = " << a << endl;
#define x first
#define y second
typedef pair<int, int> P;

const int N = 10000010;
int a[N], s[N], n, m;

bool check(int mid)
{
    for(int i = 1; i + mid <= n; i++)
    {
        if(s[i + mid] - s[i - 1] >= m) return true;
    }
    return false;
}

signed main(void)
{
    scanf("%lld%lld", &n, &m);
    for(int i = 1; i <= n; i++)
    {
        scanf("%lld", &a[i]);
        s[i] = s[i - 1] + a[i];
    }
    
    int l = 1, r = n;
    while(l < r)
    {
        int mid = l + r >> 1;
        if(check(mid)) r = mid;
        else l = mid + 1;
    }
    
    for(int i = 1; i + l <= n; i++)
    {
        if(s[i + l] - s[i - 1] >= m)
        {
            cout << i << ' ' << i + l << endl;
            break;
        }
    }
    return 0;
}
思路二

前缀和+双指针

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

const int N = 10000009;
const int mod = 1e9 + 7;
 
int n, x, a[N];
int main() {
    cin >> n >> x;
    for(int i = 1; i <= n; i ++) scanf("%d", a + i);
    int sum = 0;
    int l = 1, r = 1;
    int ans = N, ansl = N;
    while(r <= n) {
        if(sum >= x) {
            if(r - l < ans)
                ans = r - l, ansl = l;
            sum -= a[l ++];
        }
        else sum += a[r ++];
    }
    cout << ansl << ' ' << ansl + ans - 1;
    return 0;
}
思路三

单调队列维护窗口移动

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

typedef long long ll;
ll x, a[10000005], s;
int n;
int main(){
	scanf("%d%lld",&n, &x);
	for (int i = 1; i <= n; i++) scanf("%lld",&a[i]);
    int l = 0, r = n + 1;//表示答案
    int start = 1, end = 0;
    queue<int> q;//队列
    for(int i = 1; i <= n; i++)
    {
        s += a[i]; end = i;
        q.push(a[i]);
        while(q.size() && s - q.front() >= x) s -= q.front(), q.pop(), start++;
        //如果当前s - q.front() >= x那么将队头元素弹出队列,更新start,同时更新s
        if(s >= x)
        {
            if(r - l > end - start)//如果距离变小则更新
            {
                l = start;
                r = end;
            }
        }
    }
	printf("%d %d\n", l, r);
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值