大一下第十一周学习笔记

每天晚上没有特殊情况都来机房训练

周一 5.10(状压dp)

1 << j表示第j位为1 从第0位开始
第j位是否存在即 (k & (1 << j))
将第i位设置为0 S & ~(1 << i)
1 << 31 是负数最大值 小心
数据范围在20左右用状压dp,数据范围很小

 

「一本通 5.4 例 1」国王(状压dp)

一个显然的dp想法是一行一行来算

但是有个问题就是这一行和上一行的放的情况要怎么判断

显然需要知道这一行和上一行的放置情况

那么怎么知道呢,因为n小于等于10,所以我们可以用一个数来表示一行的状态

然后把这个放置情况作为dp的一维

我们发现还需要记录已经放了多少个的信息

所以用dp[i][j][S]表示前i行,已经放了j个,当前行状态为j的情况

那么转移方程为dp[i][j][S]=\sum dp[i-1][j-c[r]][pre]

c[r]表示第i行放置了多少个国王 pre表示第i-1行的放置情况

那么对于当前这一行怎么放,我们可以预处理出来所有一行合法的放的方法,不用每一次都去判断

注意一些细节

(1)方案数会很大注意用long long

(2)位运算要加括号,比如(i & (i << 1)) == 0 整个左侧外面加一个括号

(3)n位的话空间开1 << n就好,处理时用第0到n-1位

(4)判断相邻两行状态是否合法就左移与和右移与就好,非常巧妙

(5)复杂度为n * k * 2 ^ n * 2 ^ n

看起来很大,但是实际上预处理之后并没有那么多合法的状态,可以算一下合法的状态,满足斐波那契数列,大概是100左右,最后大概是1e7,是不会超时的

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
ll status[1 << 10], c[1 << 10], cnt;
ll dp[11][101][1 << 10], ans;
int n, k;

int main()
{
    scanf("%d%d", &n, &k);
    REP(i, 0, 1 << n)
    {
        if((i & (i << 1)) == 0 && (i & (i >> 1)) == 0)
        {
            status[++cnt] = i;
            for(int t = i; t; t >>= 1) c[cnt] += t & 1;
        }
    }

    _for(i, 1, cnt) dp[1][c[i]][status[i]] = 1;
    _for(i, 2, n)
        _for(j, 0, k)
            _for(p, 1, cnt)
                _for(r, 1, cnt)
                {
                    int pre = status[p], now = status[r];
                    if(j >= c[r] && (pre & now) == 0 && (pre & (now << 1)) == 0 && (pre & (now >> 1)) == 0)
                        dp[i][j][now] += dp[i - 1][j - c[r]][pre];
                }

    _for(i, 1, cnt) ans += dp[n][k][status[i]];
    printf("%lld\n", ans);

    return 0;
}

「一本通 5.4 例 2」牧场的安排(状压dp)

这道题比上一题简单,注意几个点

(1)预处理出每一行合法的状态可以加速,在循环中判断比较慢。所以要预处理一下

(2)统计答案时不要写在for里面,一开始写在for里面,由于i从2开始,如果n=1的时候就统计不到答案,要注意边dp边统计答案的时候有这个坑

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int mod = 1e8;
int dp[15][1 << 12], a[15][15], n, m, ans;
vector<int> sta[15];

int add(int a, int b) { return (a + b) % mod; }

bool check(int i, int now)
{
    _for(j, 1, m)
        if(!a[i][j] && (now & (1 << (j - 1))))
            return false;
    return (now & (now << 1)) == 0 && (now & (now >> 1)) == 0;
}

int main()
{
    scanf("%d%d", &n, &m);
    _for(i, 1, n)
        _for(j, 1, m)
            scanf("%d", &a[i][j]);

    _for(i, 1, n)
        REP(now, 0, 1 << m)
            if(check(i, now))
            {
                sta[i].push_back(now);
                if(i == 1) dp[1][now] = 1;
            }

    _for(i, 2, n)
        for(auto now: sta[i])
            for(auto pre: sta[i - 1])
                if((now & pre) == 0)
                   dp[i][now] = add(dp[i][now], dp[i - 1][pre]);

    for(auto now: sta[n]) ans = add(ans, dp[n][now]);
    printf("%d\n", ans);

    return 0;
}

「一本通 5.4 练习 1」涂抹果酱(状压dp + 三进制 + 细节)

这题两个点要注意

(1)三进制。这个就自己写个预处理和判断相邻是否合法就行

(2)这道题答案显然是第k行上方的方案数乘以下方的方案数

这里有个大坑就是我以为它放好的就以及是合法的,结果不是,可能它放的已经是不合法的,要特判。题目也没说放的已经是合法的,我自己yy,应该看题目确认一下

然后这里有个0的问题,就比如上方方案数为0的情况。这里的处理是0看作一种方案,也就是max(ans, 1),因为要相乘

我开始还想特判,就是上方为0就输出下方这样,但是这种情况上下方都为0我也要考虑

事实上,可以发现是一定有方案的,方案为0只可能为k为1或者k为n。那么都为0只有一种情况,就是只有一行

这个时候是输出1的。如果我特判那样就会输出0,就错了

所以考虑周全,上方为0,下方为0,上下方都为0

 

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int mod = 1e6;
const int N = 1e4 + 10;
int dp[N][300], n, m, k, t, ans1, ans2;
vector<int> sta;

int add(int a, int b) { return (a + b) % mod; }
int mul(int a, int b) { return 1ll * a * b % mod; }

void dfs(int k, int pre, int sum)
{
    if(k == m + 1)
    {
        sta.push_back(sum);
        return;
    }
    _for(i, 1, 3)
        if(i != pre)
            dfs(k + 1, i, sum * 10 + i);
}

bool check(int a, int b)
{
    while(a)
    {
        if(a % 10 == b % 10) return false;
        a /= 10; b /= 10;
    }
    return true;
}

int main()
{
    scanf("%d%d%d", &n, &m, &k);
    dfs(1, 0, 0);

    int p = 0;
    _for(i, 1, m)
    {
        int x; scanf("%d", &x);
        if(p == x)
        {
            puts("0");
            return 0;
        }
        p = x;
        t = t * 10 + x;
    }

    REP(i, 0, sta.size())
        if(check(t, sta[i]))
            dp[k + 1][i] = 1;
    _for(i, k + 2, n)
        REP(j, 0, sta.size())
            REP(r, 0, sta.size())
                if(check(sta[j], sta[r]))
                    dp[i][j] = add(dp[i][j], dp[i - 1][r]);
    REP(i, 0, sta.size()) ans1 = add(ans1, dp[n][i]);

    REP(i, 0, sta.size())
        if(check(t, sta[i]))
            dp[k - 1][i] = 1;
    for(int i = k - 2; i >= 1; i--)
        REP(j, 0, sta.size())
            REP(r, 0, sta.size())
                if(check(sta[j], sta[r]))
                    dp[i][j] = add(dp[i][j], dp[i + 1][r]);
    REP(i, 0, sta.size()) ans2 = add(ans2, dp[1][i]);

    printf("%d\n", mul(max(ans1, 1), max(ans2, 1)));

    return 0;
}

写完后看了别人的博客

发现三进制可以用*3 %3 写,模仿二进制。我是用* 10 % 10写的。我发现我自己写的这个比较简单一些

此外可以直接从第一行到最后一行dp,到第k行特判一下就好了,这个是我要学习的

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int mod = 1e6;
const int N = 1e4 + 10;
int dp[N][300], n, m, k, t, ans;
vector<int> sta;

int add(int a, int b) { return (a + b) % mod; }
int mul(int a, int b) { return 1ll * a * b % mod; }

void dfs(int k, int pre, int sum)
{
    if(k == m + 1)
    {
        sta.push_back(sum);
        return;
    }
    _for(i, 1, 3)
        if(i != pre)
            dfs(k + 1, i, sum * 10 + i);
}

bool check(int a, int b)
{
    while(a)
    {
        if(a % 10 == b % 10) return false;
        a /= 10; b /= 10;
    }
    return true;
}

int main()
{
    scanf("%d%d%d", &n, &m, &k);
    dfs(1, 0, 0);

    int p = 0;
    _for(i, 1, m)
    {
        int x; scanf("%d", &x);
        if(p == x)
        {
            puts("0");
            return 0;
        }
        p = x;
        t = t * 10 + x;
    }

    int T;
    REP(i, 0, sta.size())
        if(sta[i] == t)
        {
            T = i;
            break;
        }
    if(k == 1) dp[1][T] = 1;
    else REP(i, 0, sta.size()) dp[1][i] = 1;

    _for(i, 2, n)
    {
        if(i != k)
        {
            REP(j, 0, sta.size())
                REP(r, 0, sta.size())
                    if(check(sta[j], sta[r]))
                        dp[i][j] = add(dp[i][j], dp[i - 1][r]);
        }
        else
        {
            REP(r, 0, sta.size())
                if(check(sta[T], sta[r]))
                    dp[i][T] = add(dp[i][T], dp[i - 1][r]);
        }
    }

    REP(i, 0, sta.size()) ans = add(ans, dp[n][i]);
    printf("%d\n", ans);

    return 0;
}

周二 5.11(状压dp)

P2704 [NOI2001] 炮兵阵地(状压dp + 状态设计 + 滚动数组)

这题还和上上行有关,那怎么办,我一开始卡了一下

然后我发现把这个重要信息加入dp的维就好了,就多一维上一行的状态

交上去MLE了。一开始考虑到两维都是状态就想到空间会不会炸

优化空间的话

当然可以用状态的编号代替状态本身,因为合法的状态是很少的

还有一种方法是滚动数组

其实每次转移只和上一行有关

所以只需要两行,这一行和上一行

具体怎么实现呢,把所有下标都模2就行了,非常巧妙

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 110;
int dp[2][1 << 10][1 << 10], a[N][15], n, m, ans;
vector<int> sta[N];

int f(int x)
{
    int res = 0;
    for(; x; x >>= 1) res += x & 1;
    return res;
}

bool check(int p1, int p2) { return !(p1 & p2); }

int main()
{
    scanf("%d%d", &n, &m);
    _for(i, 1, n)
    {
        char s[15];
        scanf("%s", s + 1);
        _for(j, 1, m) a[i][j] = s[j] == 'P';
    }

    _for(i, 1, n)
        REP(now, 0, 1 << m)
        {
            bool ok = 1;
            _for(j, 1, m)
                if(!a[i][j] && (now & (1 << (j - 1))))
                {
                    ok = 0;
                    break;
                }
            if(ok && !(now & (now << 1)) && !(now & (now << 2))) sta[i].push_back(now); //自己和自己比较左移1和右移1是等价的
        }                                                                               //自己和别人比较左移1和有移1是不等价的

    for(int now: sta[1]) dp[1][now][0] = f(now);
    if(n >= 2)
        for(int now: sta[2])
            for(int pre: sta[1])
                if(check(now, pre))
                    dp[0][now][pre] = max(dp[0][now][pre], dp[1][pre][0] + f(now));

    _for(i, 3, n)
        for(int now: sta[i])
            for(int pre: sta[i - 1])
                for(int ppre: sta[i - 2])
                    if(check(now, pre) && check(now, ppre) && check(pre, ppre))
                        dp[i % 2][now][pre] = max(dp[i % 2][now][pre], dp[(i - 1) % 2][pre][ppre] + f(now)); //滚动数组非常巧妙

    REP(i, 0, 1 << m)
        REP(j, 0, 1 << m)
            ans = max(ans, dp[n % 2][i][j]);
    printf("%d\n", ans);

    return 0;
}

周三 5.12(状压dp)

这周末两门其中考,所以写的题会少一些

P3622 [APIO2007]动物园

自己想的时候有两个点不知道这么处理,一个是环这么处理,一个是dp方程当前状态的值要怎么处理

看了题解之后觉得太秀了,很值得我学习

(1)首先要处理出num[i][S]表示以i为开始状态为S能让多少人开心

注意这里的人一定是以i为起点的,这样统计的时候才不会乱,这就解决了我自己想时的的一个疑惑

然后判断是否合法的时候用位运算与就好了,同时配合取反

(2)dp[i][S],方程是很显然的,这里有一个问题就是怎么枚举前一个状态

注意从右往左表示i i + 1……不是从左往右。这样可以很容易用位运算来得出之前的状态,前一个状态就多了一个动物,所以就只有这个动物移不移走两种情况

(3)至于环的问题,这个解决办法比较暴力

直接枚举起始位置,然后往后推的时候只能从这个起始位置推,所以其他状态设为最小值

统计答案时也只能统计这个起始状态的答案

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a) ; i < (b); i++)
#define _for(i, a, b) for(int i = (a) ; i <= (b); i++)
using namespace std;

const int N = 1e4 + 10;
int dp[N][1 << 5], num[N][1 << 5], n, m;

int main()
{
    scanf("%d%d", &n, &m);
    _for(i, 1, m)
    {
        int st, cnt1, cnt2, fear = 0, like = 0;
        scanf("%d%d%d", &st, &cnt1, &cnt2);

        while(cnt1--)
        {
            int x; scanf("%d", &x);
            x = (x - st + n) % n; //这里注意会有负数,要+n%n
            fear |= 1 << x;
        }
        while(cnt2--)
        {
            int x; scanf("%d", &x);
            x = (x - st + n) % n;
            like |= 1 << x;
        }

        REP(S, 0, 1 << 5)
            if((S & like) || (~S & fear)) //这里位运算很秀
                num[st][S]++;
    }

    int ans = 0;
    REP(S0, 0, 1 << 5) //枚举起始位置
    {
        memset(dp, 128, sizeof(dp)); //128表示最小值 大概-2e9
        dp[0][S0] = 0;
        _for(i, 1, n)
            REP(S, 0, 1 << 5)
                dp[i][S] = max(dp[i - 1][(S & 15) << 1], dp[i - 1][(S & 15) << 1 | 1]) + num[i][S];
        ans = max(ans, dp[n][S0]);
    }
    printf("%d\n", ans);

    return 0;
}

开始刚单调队列优化dp

P1886 滑动窗口 /【模板】单调队列

单调队列可以解决这样一个问题

固定区间长度k,求每一个区间的最值

以求区间最小值为例,我们维护一个单调递增的队列

单调队列的核心是

ai 和 aj在同一个区间时,且j > i时,ai > aj, ai永远不可能为最小值

因为aj活得比ai长,ai还大

也就是有ai时一定有aj,所以ai这个元素一定可以去掉

所以我们就维护一个单调递增的队列就好了

用deque实现

队首    ……  队尾

每次插入的都是队尾,队首就是答案

#include <bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e6 + 10;
int a[N], n, k;

int main()
{
    scanf("%d%d", &n, &k);
    _for(i, 1, n) scanf("%d", &a[i]);

    deque<int> q;
    _for(i, 1, n)
    {
        if(!q.empty() && i - k + 1 > q.front()) q.pop_front();
        while(!q.empty() && a[q.back()] >= a[i]) q.pop_back();
        q.push_back(i);
        if(i >= k) printf("%d ", a[q.front()]);
    }

    puts("");
    while(!q.empty()) q.pop_back();

    _for(i, 1, n)
    {
        if(!q.empty() && i - k + 1 > q.front()) q.pop_front();
        while(!q.empty() && a[q.back()] <= a[i]) q.pop_back();
        q.push_back(i);
        if(i >= k) printf("%d ", a[q.front()]);
    }

    return 0;
}

数组实现貌似很简单,用下数组

#include <bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e6 + 10;
int a[N], q[N], n, k, l, r; //队列开N就行了

int main()
{
    scanf("%d%d", &n, &k);
    _for(i, 1, n) scanf("%d", &a[i]);

    l = 1; r = 0; //初始化注意
    _for(i, 1, n)
    {
        if(l <= r && i - k + 1 > q[l]) l++;
        while(l <= r && a[q[r]] >= a[i]) r--;
        q[++r] = i;
        if(i >= k) printf("%d ", a[q[l]]);
    }

    puts("");
    l = 1; r = 0;

    _for(i, 1, n)
    {
        if(l <= r && i - k + 1 > q[l]) l++;
        while(l <= r && a[q[r]] <= a[i]) r--;
        q[++r] = i;
        if(i >= k) printf("%d ", a[q[l]]);
    }

    return 0;
}

周四 5.13(单调队列优化dp)

poj 1821

dp[i][j]表示前i个工人,刷1~j的木板的最大值

那么dp方程很容易得出,第i个人不刷,第j块不刷,第i个人刷

max(dp[i - 1][j], dp[i][j - 1], dp[i][k] + p[i] * (j - k))

我们需要优化第三个式子

可以发现i定下,j不变时,k的区间右端点不变,左端点右移

就是滑动窗口

因此用单调队列优化,可以把枚举k的过程优化到O(1)

#include <cstdio>
#include <algorithm>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1.6e4 + 10;
const int M = 110;
int dp[M][N], q[N], n, m;
struct worker
{
    int l, p, s;
}a[M];

bool cmp(worker a, worker b)
{
    return a.s < b.s;
}

int val(int k, int i) //单调队列中元素的值
{
    return dp[i - 1][k] - k * a[i].p;
}

int main()
{
    scanf("%d%d", &n, &m);
    _for(i, 1, m) scanf("%d%d%d", &a[i].l, &a[i].p, &a[i].s);
    sort(a + 1, a + m + 1, cmp); //先排序

    _for(i, 1, m)
    {
        int l = 1, r = 0;
        _for(k, max(a[i].s - a[i].l, 0), a[i].s - 1) //k为起点的前一个
        {                                            //先预处理出k的单调队列
            while(l <= r && val(q[r], i) <= val(k, i)) r--;
            q[++r] = k;
        }

        _for(j, 1, n)
        {
            dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
            if(j >= a[i].s)
            {
                if(l <= r && j - a[i].l > q[l]) l++; //使用队列中元素时一定要非空
                if(l <= r) dp[i][j] = max(dp[i][j], val(q[l], i) + j * a[i].p);
            }
        }
    }
    printf("%d\n", dp[m][n]);

    return 0;
}

最近状态不太好,而且要期中考了

所以最近写的题会比较少,歇一歇

允许自己疲惫休息

自己不是机器,是人

看了过去的笔记, 发现我几乎每一天都在训练,每天都在写题,已经很不错了

允许自己偶尔歇一歇吧

 

周六 5.15(单调队列优化dp)

P4954 [USACO09OPEN]Tower of Hay G

这题还是非常精彩的

首先很容易有个贪心思路,就是倒序输入,每次都使得当前层尽可能小

然而这个思路是错误的

这样会导致底层过大,我们可以把从底层中抽几个草包到上面,使得总高度更大

我们考虑dp

先倒序读入,因为正着考虑很麻烦,倒着考虑就简单许多

s[i] 表示1~i草包的前缀和

dp[i]表示1~i草包的最高高度

那么就有dp[i] = dp[j] + 1 

转移条件是s[i] - s[j] >= 取得dp[j]时最底层的宽度

显然在高度相同的情况下,底层宽度越小越好

用g[j]表示这个宽度 怎么使得它最小呢

这样转移之后g[i] = s[i] - s[j]

显然j下标越大越好

那么就可以枚举第一维度i,然后枚举j,找到下标最大的满足条件的j转移

这样的时间复杂度是n方的,会超时

考虑怎么优化

我们移个项,发现s[i] >= s[j] + g[j]

是否可以把i和j分开处理?

这里的问题是每个j的s[j] + g[j]大小关系都不一定,必须枚举全部枚举一遍才能找到符合条件的j

有什么办法能快速找到符合条件的j呢

单调队列优化

我们发现,对于s[a] + g[a] >= s[b] + g[b]   a < b时

a是永远不可能转移的,因为b满足两个条件(1)值更小更可能转移(2)下标更大

因此a就可以删掉,删掉所有这样的元素之后,就会发现是一个随着下标单调增加的队列

有了单调性,就可以迅速的找到要转移的j,从队首往后遍历就行了,每个元素入队一次出队一次,复杂度O(n)

总复杂度O(n)

 

这里有一个细节搞了我很久

我写单调队列一开始会写l = 1 r = 0 我是定义的左右都是闭区间

因为往往一开始队列为空会加入一个元素,++r 使得l = 1 r = 1 存了第一个元素,这样就刚刚好

但是这道题是先取队首,再入队的,也就是一开始就要取队首元素。而我这么写的话,一开始就取了q[1]

然而这时应该队列是空的,这样就错了

所以我再循环开始前就q[++r] = 0 加入一个空元素进去,这样就不会错了

 

这道题思维量很大,代码量很小

#include <bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e5 + 10;
int dp[N], g[N], a[N], s[N], q[N], n;

int val(int x) { return g[x] + s[x]; }

int main()
{
    scanf("%d", &n);
    for(int i = n; i >= 1; i--) scanf("%d", &a[i]);
    _for(i, 1, n) s[i] = s[i - 1] + a[i];

    int l = 1, r = 0;
    q[++r] = 0;
    _for(i, 1, n)
    {
        while(l < r && val(q[l + 1]) <= s[i]) l++;
        dp[i] = dp[q[l]] + 1;
        g[i] = s[i] - s[q[l]];

        while(l <= r && val(q[r]) >= val(i)) r--;
        q[++r] = i;
    }
    printf("%d\n", dp[n]);

    return 0;
}

「一本通 5.5 例 2」最大连续和(滑动窗口)

转化成前缀和就成为一个裸的滑动窗口题目了

#include <bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 2e5 + 10;
int s[N], dp[N], q[N], n, m;

int main()
{
    scanf("%d%d", &n, &m);
    _for(i, 1, n)
    {
        int x; scanf("%d", &x);
        s[i] = s[i - 1] + x;
    }

    int l = 1, r = 1; //循环时先取出队首就l = r = 1 先加入元素就l = 1 r = 0
    _for(i, 1, n)
    {
        if(q[l] < i - m) l++;
        dp[i] = s[i] - s[q[l]];
        while(l <= r && s[q[r]] >= s[i]) r--;
        q[++r] = i;
    }

    int ans = -1e9; //习惯写ans = 0但是这里答案可能为负数,坑
    _for(i, 1, n)
        ans = max(ans, dp[i]);
    printf("%d\n", ans);

    return 0;
}

「一本通 5.5 例 3」修剪草坪

dp[i]表示i不选时的最优方案

dp[i] = max(s[i - 1] - s[j] + dp[j]) = s[i - 1] + max(dp[j] - s[j])

max(dp[j] - s[j])用单调队列优化

单调队列一般是每次都取队首的值来进行转移,然后加入元素更新队尾的值

可以根据前者判断单调性

如果看到2个条件,就可以用单调队列优化

(1)dp转移方程出现了max/min(f(j))

(2)j有一个取值范围

其实也就是滑动窗口

#include <bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int N = 1e5 + 10;
ll dp[N], s[N];
int q[N], n, k;

ll val(int x) { return dp[x] - s[x]; }

int main()
{
    scanf("%d%d", &n, &k);
    _for(i, 1, n)
    {
        int x; scanf("%d", &x);
        s[i] = s[i - 1] + x;
    }

    int l = 1, r = 1;
    _for(i, 1, n + 1) //注意最大到n + 1
    {
        if(q[l] < i - k - 1) l++;
        dp[i] = s[i - 1] + val(q[l]);

        if(i == 1) continue;
        while(l <= r && val(q[r]) <= val(i - 1)) r--;
        q[++r] = i - 1;
    }

    ll ans = 0;
    _for(i, 1, n + 1)
        ans = max(ans, dp[i]);
    printf("%lld\n", ans);

    return 0;
}

周日 5.16(单调队列)

「一本通 5.5 例 4」旅行问题

这道题我独立想的时候已经想出了思路,做了百分之80了

但是这道题的细节比较坑,直接坑死我了

 

先讨论顺时针的情况

首先令a[i] = p[i] - d[i]

如果有a[i]a[x]的和小于0,那么此时以i为起点就不行

所以这是个区间和的问题,求一个以i为起点的区间和最小值,如果最小值大于0就可以为起点

这里题目给的是环,我们把数组加倍就好,统计答案时只统计1~n的部分

所求[i, j]的值即 min(s[j] - s[i - 1]) = -s[i - 1] + min(s[j])

i <= j <= i + n - 1

用单调队列维护就好,维护一个单调递增的队列,这里后加入的元素的下标是一定更加优秀的,因为离要更新的点更近

因为这里j是可以取到i的,所以每次要先把i加入队列再更新

 

接下来是逆时针的情况,有一些关键的细节不一样

我自己做的时候天真的以为把原数组翻转一下再做一遍就好

然而改变其实非常大

这时a[i] = p[i] - d[i - 1] (画图),注意让d[0] = d[n]

这时对于[j + 1, i]的值为s[i] - s[j]  (我让变化的j都为s[j],而不是s[j - 1]之类,方便一些)

所求即min(s[i] - s[j]) = s[i] - max(s[j])

i - n <= j <= i - 1   维护一个单调递减的队列

这里j是取到i - 1的,所以先更新再加入i

最后结果是只要顺时针和逆时针有一个行就行

#include <bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int N = 2e6 + 10;
ll a[N], s[N], p[N], d[N];
int ans[N], q[N], n;

int main()
{
    scanf("%d", &n);
    _for(i, 1, n)
    {
        scanf("%lld%lld", &p[i], &d[i]);
        a[i] = a[n + i] = p[i] - d[i];
    }
    _for(i, 1, 2 * n) s[i] = s[i - 1] + a[i];

    int l = 1, r = 0;
    for(int i = 2 * n; i >= 1; i--)
    {
        if(l <= r && q[l] >= i + n) l++;   //添加元素和删除元素时注意都要先判断非空
        while(l <= r && s[q[r]] >= s[i]) r--; //先加入元素再更新
        q[++r] = i;

        if(i <= n && s[q[l]] - s[i - 1] >= 0) ans[i] = 1; //半段就好 1 ~ n
    }

    d[0] = d[n]; //注意这里
    _for(i, 1, n) a[i] = a[n + i] = p[i] - d[i - 1]; //逆时针的时候要改变
    _for(i, 1, 2 * n) s[i] = s[i - 1] + a[i];

    l = 1, r = 0;
    _for(i, 1, 2 * n)
    {
        if(l <= r && q[l] < i - n) l++;   //添加元素和删除元素时注意都要先判断非空
        if(i > n && s[i] - s[q[l]] >= 0) ans[i - n] = 1; //半段 n + 1 ~ 2n

        while(l <= r && s[q[r]] <= s[i]) r--; //先更新再加入元素
        q[++r] = i;
    }

    _for(i, 1, n) puts(ans[i] ? "TAK" : "NIE"); //顺或逆可行就可行

    return 0;
}

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值