跳转至

训练记录

2024.7.17 训练记录

BNDSOJ USACO - Connect The Cows

Connect The Cows

一开始拿到这道题,脑残直接开始一格一格 \(dfs\),难想、复杂又容易写错……实际上,由于约翰 只会在牛处转向,我们只需要在原点 \((0, 0)\) 和牛之间 \(dfs\) 即可

我们枚举每只牛的坐标,如果有两只牛 \(x\) 坐标相同或 \(y\) 坐标相同就在它们之间连边;注意,如果牛的 \(x\) 坐标为 \(0\)\(y\) 坐标也为 \(0\),也别忘了在它与原点之间连边

\(dfs\) 时,我们记录当前节点编号、方向 与经过的牛的数量;别忘了记录方向!枚举与当前点直接连边的点,特判下个点是 \(0\) 且已经经过所有牛 的情况 (因为如果没有经过所有牛回到原点是没有意义的,反正也无法改变方向);若下个点为牛,必须转向,无需考虑不改变方向的情况 (假设从起始牛的位置向下走到中间牛,如果中间牛处不改变方向,只有在该头牛下方还有一头牛时有意义;又由于我们的连边操作,起始牛一定会向这一头牛连一条边,因此如果我们考虑不改变方向的情况,答案会被重复计算

代码

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 15;

int N, ans;

struct Cow{
    int x;
    int y;
}cow[MAXN];

struct Edge{
    int to;
    int dir;
};

vector <Edge> G[MAXN];

void show(){
    cout << "show: " << endl;
    for (int i=0; i<=N; i++){
        for (int j=0; j<G[i].size(); j++){
            cout << i << " " << G[i][j].to << ", dir = " << G[i][j].dir << endl;
        }
    }
}

bool vis[MAXN];

void dfs(int node, int dir, int cnt){
    if (node == 0 && cnt == N+1){
        ans++;
        return; 
    }

    for (int i=0; i<G[node].size(); i++){
        if (dir == G[node][i].dir){
            if (G[node][i].to == 0 && cnt == N+1){
                dfs(G[node][i].to, dir, cnt);
            }

            if (G[node][i].to != 0 && vis[G[node][i].to] == false){
                vis[G[node][i].to] = true;
                for (int j=0; j<4; j++){
                    if (j != dir){
                        dfs(G[node][i].to, j, cnt+1);
                    }   
                }
                vis[G[node][i].to] = false;
            }
        }
    }   
}

int main(){
    scanf("%d", &N);

    for (int i=1; i<=N; i++){
        scanf("%d%d", &cow[i].x, &cow[i].y);
        if (cow[i].x == 0){
            if (cow[i].y > 0){
                G[0].push_back({i, 0});
                G[i].push_back({0, 2});
            }
            else{
                G[0].push_back({i, 2});
                G[i].push_back({0, 0});
            }
        }
        else if (cow[i].y == 0){
            if (cow[i].x > 0){
                G[0].push_back({i, 3});
                G[i].push_back({0, 1});
            }
            else{
                G[0].push_back({i, 1});
                G[i].push_back({0, 3});
            }
        }
    }

    for (int i=1; i<=N; i++){
        for (int j=i+1; j<=N; j++){
            if (cow[i].x == cow[j].x){
                if (cow[i].y > cow[j].y){
                    G[i].push_back({j, 2}); 
                    G[j].push_back({i, 0});
                }
                else{
                    G[i].push_back({j, 0});
                    G[j].push_back({i, 2});
                }
            }
            else if (cow[i].y == cow[j].y){
                if (cow[i].x > cow[j].x){
                    G[i].push_back({j, 1}); 
                    G[j].push_back({i, 3});
                }
                else{
                    G[i].push_back({j, 3});
                    G[j].push_back({i, 1});
                }
            }
        }
    }

    // show();

    vis[0] = true;
    dfs(0, 0, 1);
    dfs(0, 1, 1);
    dfs(0, 2, 1);
    dfs(0, 3, 1);
    printf("%d", ans);
    return 0;
}

BNDSOJ USACO - Wrong Directions

Wrong Directions

我们如果每次更改一个字符串,再从头扫一遍,时间复杂度为 \(O(n^2)\),显然会超时

我们优化暴力,考虑将原字符串分为三段,具体的,为更改的字符、更改的字符前段、更改的字符后段三段;我们考虑对长度为 \(N\) 的原字符串 \(s[i]\) 进行预处理,由于我们 只需要知道更改前段走到哪里、更改后段相较于原点的相对距离,因此预处理出 前缀位置数组 \(before[i]\),记录按照子串 \(s[1...i]\) 操作后走到的位置;再处理出 后缀位置数组 \(after[i]\),记录按照子串 \(s[i...N]\) 操作后走到的位置 (起始方向都默认向北);

我们发现,\(before[i]\) 数组是容易维护的,但由于 \(after[i]\) 数组是倒着处理的,不能按照常规方法维护;考虑分类讨论第 \(i\) 位的字符种类:

  1. 如果为 \(F\),那很好办,直接令 \(after[i].x = after[i+1].x\)\(after[i].y = after[i+1].y+1\) 即可,因为默认起始方向向北,向前走一步即为向北走一步,体现在 \(y\) 坐标的变化上
  2. 如果为 \(L\),那么原来的 \(y\) 移动量变成了现在的 \(x\) 移动量,原来的 \(x\) 移动量变成了现在的 \(y\) 移动量;至于方向,我们可以画个图辅助理解

左转图片

此时有 \(after[i].x = -after[i+1].y, after[i].y = after[i+1].x\)

  1. 如果为 \(R\),那么原来的 \(y\) 移动量还是变成了现在的 \(x\) 移动量,原来的 \(x\) 移动量也还是变成了现在的 \(y\) 移动量;其实图与左转时是相同的,只不过左转时的 \(after[i]\) 相当于右转时的 \(after[i+1]\)、左转时的 \(after[i+1]\) 相当于右转时的 \(after[i]\);类似的,此时有 \(after[i].x = after[i+1].y, after[i].y = -after[i+1].x\)

这样,我们就能够处理出 \(after[i]\) 数组了;实际更改第 \(i\) 位时,只需要先将位置变更为 \(before[i-1]\),讨论第 \(i\) 位的变化,再两坐标分别增加 \(after[i+1].x\)\(after[i+1].y\) 即可;

实现时,注意我们在处理 \(after[i]\) 数组时默认方向向北,但更改字符时可能会改变方向,使方向不是北,因此我们需要考虑方向的变化;具体的,如果方向变成西 / 东,那相当于一次 左转 / 右转,否则相当于一次 按原点对称翻转

代码

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e5+5;

string s;
int ans;

map <int, map<int, bool> > vis;

struct Node{
    int x;
    int y;
    int dir;
}before[MAXN], after[MAXN];

pair<int, int> getnex(int x, int y, int dir){
    int nx, ny;

    if (dir == 0){
        nx = x;
        ny = y+1;
    }       
    else if (dir == 1){
        nx = x-1;
        ny = y;
    }
    else if (dir == 2){
        nx = x;
        ny = y-1;
    }
    else{
        nx = x+1;
        ny = y;
    }

    return {nx, ny};
}

pair <int, int> offset(int x, int y, int dir){
    if (dir == 1){
        return {-y, x};
    }
    if (dir == 0){
        return {x, y};
    }
    if (dir == 2){
        return {-x, -y};
    }
    if (dir == 3){
        return {y, -x};
    }
}

int main(){
    cin >> s;
    before[0].x = 0;
    before[0].y = 0;
    before[0].dir = 0;

    for (int i=1; i<=s.size(); i++){
        if (s[i-1] == 'L'){
            before[i] = before[i-1];
            before[i].dir = (before[i].dir+1)%4;
        }   
        else if (s[i-1] == 'R'){
            before[i] = before[i-1];
            before[i].dir = (before[i].dir+3)%4;
        }
        else{
            int nx, ny;
            nx = getnex(before[i-1].x, before[i-1].y, before[i-1].dir).first;
            ny = getnex(before[i-1].x, before[i-1].y, before[i-1].dir).second;
            before[i] = {nx, ny, before[i-1].dir};
        }
    }

    for (int i=s.size(); i>=1; i--){
        if (s[i-1] == 'L'){
            after[i].x = -after[i+1].y;
            after[i].y = after[i+1].x;
        }
        else if (s[i-1] == 'R'){
            after[i].x = after[i+1].y;
            after[i].y = -after[i+1].x;
        }
        else{
            after[i].x = after[i+1].x;
            after[i].y = after[i+1].y+1;
        }
    }

    for (int i=1; i<=s.size(); i++){
        int ansx = before[i-1].x, ansy = before[i-1].y, ansdir = before[i-1].dir;

        if (s[i-1] == 'F'){
            int offsetx = offset(after[i+1].x, after[i+1].y, (ansdir+1)%4).first;
            int offsety = offset(after[i+1].x, after[i+1].y, (ansdir+1)%4).second;
            int nx = ansx+offsetx;
            int ny = ansy+offsety;
            //cout << nx << " " << ny << endl;
            if (!vis[nx][ny]){
                vis[nx][ny] = true;
                ans++;
            }

            offsetx = offset(after[i+1].x, after[i+1].y, (ansdir+3)%4).first;
            offsety = offset(after[i+1].x, after[i+1].y, (ansdir+3)%4).second;
            nx = ansx+offsetx;
            ny = ansy+offsety;
            //cout << nx << " " << ny << endl;
            if (!vis[nx][ny]){
                vis[nx][ny] = true;
                ans++;
            }
        }
        else if (s[i-1] == 'L'){
            int offsetx = offset(after[i+1].x, after[i+1].y, (ansdir+3)%4).first;
            int offsety = offset(after[i+1].x, after[i+1].y, (ansdir+3)%4).second;
            int nx = ansx+offsetx;
            int ny = ansy+offsety;
            //cout << nx << " " << ny << endl;
            if (!vis[nx][ny]){
                vis[nx][ny] = true;
                ans++;
            }

            offsetx = offset(after[i+1].x, after[i+1].y+1, ansdir).first;
            offsety = offset(after[i+1].x, after[i+1].y+1, ansdir).second;
            nx = ansx+offsetx;
            ny = ansy+offsety;
            //cout << nx << " " << ny << endl;
            if (!vis[nx][ny]){
                vis[nx][ny] = true;
                ans++;
            }
        }
        else if (s[i-1] == 'R'){
            int offsetx = offset(after[i+1].x, after[i+1].y, (ansdir+1)%4).first;
            int offsety = offset(after[i+1].x, after[i+1].y, (ansdir+1)%4).second;
            int nx = ansx+offsetx;
            int ny = ansy+offsety;
            //cout << nx << " " << ny << endl;
            if (!vis[nx][ny]){
                vis[nx][ny] = true;
                ans++;
            }

            offsetx = offset(after[i+1].x, after[i+1].y+1, ansdir).first;
            offsety = offset(after[i+1].x, after[i+1].y+1, ansdir).second;
            nx = ansx+offsetx;
            ny = ansy+offsety;
            //cout << nx << " " << ny << endl;
            if (!vis[nx][ny]){
                vis[nx][ny] = true;
                ans++;
            }
        }
    }

    printf("%d", ans);
    return 0;
}

BNDSOJ USACO - Three Lines

Three Lines

看到题,想到一个看起来很对的贪心:我们按照每行有多少头牛从大到小排序、每列有多少头牛从大到小排序,删去牛最多的一行、牛最多的一列,再判断剩下的牛是否能够用一行或一列覆盖

我们感性理解一下这个贪心;如果我们用两行一列或两列一行删牛,一定会删一行或一列,那么对于删的这一行 / 一列,我们希望删完之后尽可能减小接下来再删列 / 行 的压力,因此我们删包含牛最多的一行 / 一列

这时候,你发现这样感性理解有一个很显然的漏洞:如果我们根本不用行或不用列,直接用三行 / 三列删牛怎么办?是的,贪心在这种情况下正确性无法保证,因此我们需要 特判使用三行或三列能否删掉所有牛

不特判的反例如下:

Text Only
1
2
3
CCC
 CC
CC
这样我们会删除第一行和第二列,剩余的牛无法用一行或一列覆盖;但实际上,我们直接用三行或者三列就可以删掉所有的牛

代码

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 5e4+5;

int N, row, col, trow, tcol;

struct Cow{
    int x;
    int y;
}cow[MAXN];

map <int, bool> vis1, vis2, t1, t2;
map <int, int> rc, cr, rc2, cr2;

int main(){
    scanf("%d", &N);

    for (int i=1; i<=N; i++){
        scanf("%d%d", &cow[i].x, &cow[i].y);
        if (vis1[cow[i].x] == false){
            vis1[cow[i].x] = true;
            row++;
        }
        if (vis2[cow[i].y] == false){
            vis2[cow[i].y] = true;
            col++;
        }
    }

    if (row <= 3 || col <= 3){
        printf("1");
        return 0;
    }

    for (int i=1; i<=N; i++){
        rc[cow[i].x]++;
        cr[cow[i].y]++;
    }
    for (auto it = rc.begin(); it != rc.end(); it++){
        rc2[(*it).second] = (*it).first;
    }
    for (auto it = cr.begin(); it != cr.end(); it++){
        cr2[(*it).second] = (*it).first;
    }
    auto brc = rc2.end();
    brc--;
    auto bcr = cr2.end();
    bcr--;
    int mrc = (*brc).second;
    int mcr = (*bcr).second;
    // cout << mrc << " " << mcr;

    for (int i=1; i<=N; i++){
        if (cow[i].x == mrc || cow[i].y == mcr){
            continue;
        }
        if (t1[cow[i].x] == false){
            t1[cow[i].x] = true;
            trow++;
        }
        if (t2[cow[i].y] == false){
            t2[cow[i].y] = true;
            tcol++;
        }
    }

    if (trow == 1 || tcol == 1){
        printf("1");
        return 0;
    }
    printf("0");
    return 0;
}

BNDSOJ USACO - Islands

Islands

我们考虑暴力,如果暴力枚举水的高度,然后扫一遍原序列统计岛屿的个数,时间复杂度为 \(O(HN)\)\(H\) 是值域,这里为 \(1e9\),显然会爆炸

我们发现值域实在是太大了,怎样避免枚举每个值呢?注意到,如果岛屿个数发生变化,临界点 必然是水的高度与之前某一还没有被淹没的陆机的高度相同时;又因为随着水位逐渐涨高,被淹没的陆地高度也会相应变高,因此我们 对陆地按照高度从小到大排序,依次处理淹没即可;这样时间复杂度变为 \(O(N^2)\),还是会超时

我们考虑优化如何统计岛屿个数。考虑水位上涨引起的岛屿个数的变化;具体的,设淹没前有 \(cnt\) 个岛屿,当前为第 \(i\) 块陆地被淹没,分情况讨论:

  1. \(i-1\) 块与第 \(i+1\) 块陆地都被淹没,此时 \(cnt=cnt-1\)
  2. \(i-1\) 块与第 \(i+1\) 块陆地有且仅有一块被淹没,此时 \(cnt\) 不变
  3. \(i-1\) 块与第 \(i+1\) 块陆地都没有被淹没,此时 \(cnt=cnt+1\)

那么如果淹没的陆地的旁边是岸怎么办?没关系,初始时令第 \(0\) 块陆地 (左岸) 与第 \(N+1\) 块陆地 (右岸) 也被淹没,之后正常统计即可;这样维护每次更改的时间复杂度就为 \(O(1)\)

时间复杂度 \(O(NlogN)\)

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e5+5;

int N, ans;
int h[MAXN];
bool deleted[MAXN];

struct Node{
    int val;
    int pos;
}sorth[MAXN];

bool cmp(Node a, Node b){
    return a.val < b.val;
}

int main(){
    scanf("%d", &N);

    for (int i=1; i<=N; i++){
        scanf("%d", &h[i]);
        sorth[i].val = h[i];
        sorth[i].pos = i;
    }
    sort(sorth+1, sorth+N+1, cmp);
    deleted[0] = true;
    deleted[N+1] = true;

    int recenth = -1, cnt = 1;
    for (int i=1; i<=N; ){
        if (sorth[i].val != recenth){
            ans = max(ans, cnt);
            recenth = sorth[i].val;
        }   
        else{
            int delpos = sorth[i].pos;
            deleted[delpos] = true;
            if (deleted[delpos-1] && deleted[delpos+1]){
                cnt--;
            }
            else if ((!deleted[delpos-1]) && (!deleted[delpos+1])){
                cnt++;
            }
            i++;
        }
    }

    ans = max(ans, cnt);
    printf("%d", ans);
    return 0;
}

P5120 USACO - Convention II S

P5120

考虑按照题意模拟,令 \(topend\) 为当前吃草的牛吃完草的时间,\(cow[i].a\) 为第 \(i\) 头牛到达的时间,\(cow[i].t\) 为第 \(i\) 头牛吃草的时间;由于每次来的牛,按照资历 (即输入先后顺序) 排序,因此我们 定义一个优先队列,也按照输入先后顺序排序,模拟排队

当第 \(i\) 头牛到达时,我们分情况讨论如下:

  1. 草地上有牛在吃,那么让第 \(i\) 头牛直接入队等待

  2. 草地上没有牛在吃

2.1 队列中没有牛等待,直接让第 \(i\) 头牛吃即可;注意,不是入队,此时直接令 \(topend = cow[i].a+cow[i].t\)

2.2 队列中有牛等待,那么 让队首的牛去吃草 ,同时 统计队首的牛的等待时间 (注意不是立马统计第 \(i\) 头牛的等待时间),队首的牛在 \(q.top().a\) 时前来,在 \(topend\) 时吃上草,因此其等待时间为 \(topend-q.top().a\);别忘了在统计后更新 \(topend\),加上 \(q.top().t\),因为当前队首牛也得吃一会;注意,如果当前队首牛吃完的时间 (也就是更新后的 \(topend\)) 仍然小于 \(cow[i].a\),那么我们 重新处理第 \(i\) 头牛的情况,因为可能会需要更改出队信息,即队列中一些看起来在排队的牛实际上已经吃完了;当然,如果更新后的 \(topend\) 大于等于 \(cow[i].a\),那么说明还得等,直接让第 \(i\) 头牛入队即可

注意,处理完所有牛的到达操作后,可能队列里还有牛,因为会有没吃完的情况;此时按一样的方法维护队列,依次让队首牛出队,直到队列里没有牛即可

代码

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e5+5;

int N, ans;

struct Cow{
    int a;
    int t;
    int id;

    bool operator > (const Cow c) const {
        return c.id < id;
    }
}cow[MAXN];

priority_queue <Cow, vector<Cow>, greater<Cow> > q;

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

int main(){
    scanf("%d", &N);

    for (int i=1; i<=N; i++){
        scanf("%d%d", &cow[i].a, &cow[i].t);
        cow[i].id = i;
    }
    sort(cow+1, cow+N+1, cmp);
    // cout << cow[1].id << endl;

    int topend = cow[1].a+cow[1].t;
    for (int i=2; i<=N; i++){
        if (topend <= cow[i].a){
            if (q.empty()){
                topend = cow[i].a+cow[i].t;
            }
            else{
                ans = max(ans, topend-q.top().a);
                topend += q.top().t;
                q.pop();
                if (topend < cow[i].a){
                    i--;
                }
                else{
                    q.push(cow[i]);
                }
            }
        }
        else{
            q.push(cow[i]);
        }   
    }
    while (!q.empty()){
        ans = max(ans, topend-q.top().a);
        topend += q.top().t;
        q.pop();
    }

    printf("%d", ans);
    return 0;
}

2024.7.16 训练记录

BNDSOJ USACO - Grazing Patterns

Grazing Patterns

看到题,首先考虑暴力 \(bfs\);我们将两个起点都存入队列中,当两点移动到同一位置且走过步数之和为 \(25-K\) 时更新答案;实际上,实现起来发现这种做法较为烦琐,因此我们先考虑对题意进行一些简化

实际上,题面中两牛各走出了一条有向路径,且这两条有向路径 终点相同,且 长度相同;考虑对路径进行转化,由于两路径终点相同,我们可以将米尔德里德走出的路径 反向 一下,这样米尔德里德走出的路径起点就为贝茜走出的路径的终点,米尔德里德走出的路径的终点即为 \((5, 5)\);这样两牛的路径就被 合成为一条从 \((1, 1)\)\((5, 5)\) 的路径,又因为合称前两路径长度相同,原相交点实际上也是可以唯一确定的;

因此,我们实际要求的就是从 \((1, 1)\) 走到 \((5, 5)\) 且恰好覆盖了整个地图的路径数量,\(dfs\) 即可

代码

C++
#include<bits/stdc++.h>
using namespace std;
int K, ans=0;
int mp[6][6];
int mv[4][2] = {1, 0, -1, 0, 0, 1, 0, -1};
bool vis[6][6];

void dfs(int x, int y, int step){
    if (x == 5 && y == 5 && step == 25-K){
        ans++;
        return;
    }

    for (int i=0; i<4; i++){
        int nx = x+mv[i][0];
        int ny = y+mv[i][1];

        if (nx<1 || ny<1 || nx>5 || ny>5){
            continue;
        }
        if (mp[nx][ny] == -1 || vis[nx][ny]){
            continue;
        }

        vis[nx][ny] = true;
        dfs(nx, ny, step+1);
        vis[nx][ny] = false;
    }   
}

int main(){
    scanf("%d", &K);

    for (int i=1; i<=K; i++){
        int x, y;
        scanf("%d%d", &x, &y);
        mp[x][y] = -1;
    }

    vis[1][1] = true;
    dfs(1, 1, 1);
    printf("%d", ans);
    return 0;
}

P1884 USACO - Overplanting S

P1884

这道题实际上是扫描线板子,可是我不会呀

首先考虑暴力。有一个很显然的做法,对于一个左上角、右下角坐标分别为 \((lx, ly), (rx, ry)\) 的矩形,枚举 \(x(lx < x \le rx)\)\(y(ry \le y < rx)\),开一个 \(vis\) 数组记录这样的点 \((x, y)\) 是否统计过,如果没有统计过就更新答案并更新 \(vis\) 数组;这种做法很显然会超时

我们注意到暴力做法实际上是通过枚举矩形内每个点来保证不会重复计算面积,这种做法比较直观但很低效;其实问题的关键就在于 如何保证不会重复计算面积,考虑新加入矩形 \((lx, ly), (rx, ry)\),对于之前的任一矩形 \((lx', ly'), (rx', ry')\),新加入的矩形可能会与该矩形产生重叠,那么我们可以考虑把原来的矩形 分割为一些与新矩形不重叠的矩形,这样就解决了重复计算的问题

实现细节上,我们如果直接考虑新矩形整体与原矩形整体的位置关系,会发现有很多情况需要讨论,比较麻烦;其实我们可以直接考虑新矩形 各边 与原矩形对应各边的关系,即 按边分割;举个例子,如果新矩形的下边比原矩形的下边高,说明原矩形底部需要分割出一块小矩形;可以参照下图理解,当新矩形被原矩形完全包含时,我们实际上将原矩形分割成了四块

分割图片

代码

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 2e6+5;

int N, front=0, rear=0;
long long ans;

struct Rec{
    int lx;
    int ly;
    int rx;
    int ry;
}rec[MAXN];

void Cut(Rec rec1, Rec rec2){
    // cout << "use!" << endl;
    if (rec2.ly <= rec1.ry || rec2.ry >= rec1.ly || rec2.lx >= rec1.rx || rec2.rx <= rec1.lx){
        rec[rear].lx = rec1.lx;
        rec[rear].ly = rec1.ly;
        rec[rear].rx = rec1.rx;
        rec[rear].ry = rec1.ry;
        rear++;
        return;
    }   
    if (rec2.ly < rec1.ly){
        rec[rear].lx = rec1.lx;
        rec[rear].ly = rec1.ly;
        rec[rear].rx = rec1.rx;
        rec[rear].ry = rec2.ly;
        rear++;
    }
    if (rec2.ry > rec1.ry){
        rec[rear].lx = rec1.lx;
        rec[rear].ly = rec2.ry;
        rec[rear].rx = rec1.rx;
        rec[rear].ry = rec1.ry;
        rear++;
    }
    if (rec2.lx > rec1.lx){
        rec[rear].lx = rec1.lx;
        rec[rear].ly = min(rec1.ly, rec2.ly);
        rec[rear].rx = rec2.lx;
        rec[rear].ry = max(rec1.ry, rec2.ry);
        rear++;
    }
    if (rec2.rx < rec1.rx){
        rec[rear].lx = rec2.rx;
        rec[rear].ly = min(rec1.ly, rec2.ly);
        rec[rear].rx = rec1.rx;
        rec[rear].ry = max(rec1.ry, rec2.ry);
        rear++;
    }
    return;
}

int main(){
    scanf("%d", &N);

    for (int i=1; i<=N; i++){
        scanf("%d%d%d%d", &rec[rear].lx, &rec[rear].ly, &rec[rear].rx, &rec[rear].ry);
        int recentrear = rear;
        rear++;

        for (int j=front; j<recentrear; j++){
            Cut(rec[j], rec[recentrear]);
            front++;    
        }
    }

    for (int i=front; i<=rear; i++){
        // cout << "id " << i << ": " << rec[i].lx << " " << rec[i].ly << " " << rec[i].rx << " " << rec[i].ry << endl;
        ans += 1ll*(rec[i].rx-rec[i].lx)*(rec[i].ly-rec[i].ry); 
    }

    printf("%lld", ans);
    return 0;
}

P4087 USACO - Milk Measurement S

P4087

记产量最大的牛的个数 \(cnt1\),我们发现,如果照片需要被调整,要么是 \(cnt1\) 发生变化,要么就是具体的牛有变化;按照这个思路,我们先具体分析一下如果照片需要被改变,可能需要满足什么条件

具体的,我们考虑分情况讨论:记 \(recentmilk\)测量前 的最大产量,\(recentcnt\) 为测量前最大产量的个数,\(maxmilk\)测量后 的最大产量,\(maxcnt\) 为测量后最大产量的个数


  1. 测量后最大产量不变 (\(recentmilk = maxmilk\))

1.1 测量后 最大个数发生变化,即此时 \(recentcnt \ne maxcnt\),那么显然照片要发生变化

1.2 测量后最大个数不变化,即此时 \(recentcnt = maxcnt\) ,我们反证:如果照片还会发生改动,那么只能是测量后具体的牛发生了变化,即有一头牛上来有一头牛下去,但一次测量只能改变一头牛,因此这种情况不成立


  1. 测量后最大产量变化 (\(recentmilk \ne maxmilk\))

2.1 测量后 最大个数发生变化,显然照片也要发生变化

2.2 如果最大个数不变:

2.2.1 一头测量前产量最大的牛经过测量后产量发生变化。再分产量变大或变小两种情况讨论

2.2.1.1 其产量变大。此时最大个数会发生变化 (变成 \(1\)),又由于最大个数不变,那么原本的最大个数就是 \(1\),这样对照片不会产生影响

2.2.1.2 其产量变小。如果其变小后仍然产量最大,此时又由于最大个数不变,那就说明原来的最大个数是 \(1\),现在还是 \(1\),此时对照片不产生影响。另一种情况,如果其变小后不再是产量最大的牛,说明变小后有另一些牛的产量比它大了,那么照片需要变化

2.2.2 一头测量前产量不是最大的牛经过测量后产量变为最大。此时最大个数变为 \(1\),那么原本的最大个数也为 \(1\),又因为此时具体的牛发生变化,所以照片需要变化


我们对上面的讨论进行总结,如果照片会发生变化,可能有如下情况

  1. 测量后 最大个数发生变化,即 \(recentcnt \ne maxcnt\)
  2. 测量后最大产量发生变化,而该次变化的牛为测量前产量最大的牛,其产量变小,变化后不再是产量最大的牛;又因为如果其产量变大,一定是产量最大的牛,所以该条也可以表述为测量后最大产量发生变化,且该次变化的牛为测量其产量最大的牛,其变化后不再是产量最大的牛;进一步的,如果该次变化的牛不是测量前产量最大的牛,原本产量最大的牛最终也不会是产量最大的牛;因此可以进一步表述为 测量后最大产量发生变化,且变化后原本最大产量的牛不再是现在最大产量的牛
  3. 测量后最大产量发生变化,且为测量前产量非最大的牛发生变化

现在我们就解决了何时照片会变化的问题,全程只需要维护 \(recentmilk, maxmilk, recentcnt, maxcnt\) 与支持给定奶牛查询奶牛当前产奶量、给定奶量查询当前产奶量为该奶量的奶牛有多少个的数据结构;容易发现这些数据结构只需要支持单点改单点查,因此我们没必要开线段树,只开 \(map\) 即可

我们枚举每一次测量,按题意对两个 \(map\) 进行维护,维护前进行 \(recentmilk = maxmilk, recentcnt = maxcnt\) 存储上一次的信息,维护后更新 \(maxmilk\)\(maxcnt\),再记录本次更改是否更改的是最大牛,更改后原本最大牛是否还是最大牛,按照总结出的条件进行维护即可

代码

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e5+5, INF = 2e9;

int N, G, ans;

struct Measure{
    int day;
    int id;
    long long c;
}m[MAXN];

bool cmp(Measure a, Measure b){
    return a.day < b.day;
}

map <int, long long> milk;
map <int, int> cnt;

int main(){
    scanf("%d%d", &N, &G);

    for (int i=1; i<=N; i++){
        scanf("%d%d%lld", &m[i].day, &m[i].id, &m[i].c);
    }
    sort(m+1, m+N+1, cmp);

    long long maxmilk=0, recentmilk=0;
    int maxcnt=INF, recentcnt=0;
    bool flagbefore = false, flagafter = false;
    cnt[0] = INF;

    for (int i=1; i<=N; i++){
        if (milk[m[i].id] == maxmilk){
            flagbefore = true;
        }
        else{
            flagbefore = false;
        }
        cnt[milk[m[i].id]]--;
        if (cnt[milk[m[i].id]] == 0){
            cnt.erase(milk[m[i].id]);
        }
        milk[m[i].id]+=m[i].c;
        cnt[milk[m[i].id]]++;

        recentmilk = maxmilk;
        recentcnt = maxcnt;
        auto it = cnt.end();
        it--;
        maxmilk = (*it).first;
        maxcnt = (*it).second;
        if (milk[m[i].id] == maxmilk){
            flagafter = true;
        }
        else{
            flagafter = false;
        }

        if (maxcnt != recentcnt){
            ans++;
        }
        else if ((flagbefore == false || flagafter == false) && (maxmilk != recentmilk)){
            ans++;
        }
    }
    printf("%d", ans);
    return 0;
}

P4089 USACO - The Bovine Shuffle

P4089

我们观察到,如果 \(i, a[i], a[a[i]], ...\) 形成一个环,则在这个环里的奶牛只会在这个环里按顺序来回跳;又因为初始时这个环上的每个点都有奶牛,则 任意时刻这个环上也都有奶牛

因此,我们将每个 \(i\)\(a[i]\) 连边,拓扑排序 判环即可,最终拓扑排序删不掉的点的个数就是答案

代码

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e5+5;

int N, ans;
int a[MAXN], in[MAXN];

vector <int> G[MAXN];

void TopoSort(){
    queue <int> S;

    for (int i=1; i<=N; i++){
        if (in[i] == 0){
            S.push(i);
        }   
    }

    while (!S.empty()){
        int recent = S.front();
        S.pop();
        ans++;

        for (int i=0; i<G[recent].size(); i++){
            in[G[recent][i]]--;
            if (in[G[recent][i]] == 0){
                S.push(G[recent][i]);
            }
        }
    }
}

int main(){
    scanf("%d", &N);

    for (int i=1; i<=N; i++){
        scanf("%d", &a[i]);
        G[i].push_back(a[i]);
        in[a[i]]++;
    }

    TopoSort();
    printf("%d", N-ans);
    return 0;
}

P4378 Out Of Sorts S

P4378

本质上,一次循环会消掉每个位置上的一个逆序对;因此,逆序对最多的一个位置的逆序对个数 \(+1\) 即为答案;

我们感性理解,将数列从小到大排序,认为每个元素前移的距离反映了其原位置上逆序对的数量;但是这么做有问题,每个元素前移的距离并不能真正代表其原位置上逆序对数量,举个例子,设原数列为 \(3, 2, 1\),排序后为 \(1, 2, 3\),元素 \(2\) 前移的距离为 \(0\),但实际上其原位置上有一个逆序对 \((3, 2)\)

问题出在哪呢?我们发现,\(2\) 前面有一个 \(1\),顶掉了一个前移的距离,这说明若一个元素 \(a_i\) 后有比它的值还小的元素 \(a_j (a_j < a_i)\),排序后 \(a_j\) 会变到 \(a_i\) 前面影响原 \(a_i\) 位置上的逆序对个数统计;但又由于我们 只关注逆序对数最大的位置,由于 \(a_i > a_j\)\(a_i\) 及以前所有大于等于它的元素都会大于 \(a_j\),所以 \(a_j\) 位置上的逆序对个数严格大于 \(a_i\),这样我们只关注 \(a_j\) 即可,\(a_i\) 位置上的逆序对数量统计差异对答案 没有影响

因此,我们将原序列排序,统计元素的 最大移动距离,最后 \(+1\) 即可 (为什么 \(+1\)? 因为最终冒泡排序完成后还要再扫一遍确定已经不需要再来一轮)

代码

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e5+5;

int N, ans;

struct Node{
    int val;
    int pos;
}num[MAXN];

bool cmp(Node a, Node b){
    if (a.val == b.val){
        return a.pos < b.pos;
    }
    return a.val < b.val;
}

int main(){
    scanf("%d", &N);

    for (int i=1; i<=N; i++){
        scanf("%d", &num[i].val);
        num[i].pos = i;
    }
    sort(num+1, num+N+1, cmp);

    for (int i=1; i<=N; i++){
        ans = max(ans, num[i].pos-i);
    }
    printf("%d", ans+1);
    return 0;
}

P4380 USACO - Multiplayer Moo S

P4380

第一问简单 \(dfs\) 即可,关键在于第二问

首先有一个思路,直接枚举每个相邻且颜色不同的方格,以这两个颜色为基准颜色,暴力进行 \(dfs\) 统计答案;这样肯定可以写,但比较麻烦,这里有一个更好的思路

在进行第一遍 \(dfs\) 时,我们实际上已经能够得出每个连通块的基本信息 (如大小,颜色),没必要在第二遍再 \(dfs\) 一次;因此,第一遍时我们记录下每个连通块的大小 \(siz\) 与颜色 \(col\),开结构体存储,同时开二维数组 \(belong[i][j]\) 表示 \((i, j)\) 格属于哪个连通块;求第二问时,我们仍然枚举每个方格,但这次是为了在相邻的连通块间 连边

连好后,我们得到了一个无向连通图,希望求出相邻的、颜色为两个颜色之一的连通块的大小之和;由于连通块的性质,图上直接相连的两连通块颜色必然不同,因此枚举每个连通块及颜色,\(dfs\) 即可

实现细节上,首先,连边时记得开 \(vis\) 数组记录相邻的连通块是否已经连过边了,不要重复连边;其次,可以在连边时同时记录每个连通块相邻的连通块有什么颜色 (即 \(dfs\) 时该连通块除自身颜色的第二个颜色都有什么选择),方便 \(dfs\);每次枚举连通块,直接把该连通块所有的颜色可能 \(dfs\) 完,以后再遇到该连通块直接 \(continue\) 即可

代码

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 505, MAXL = 7e4+5;

int N, cnt, recentsiz, ans1, ans2;
int mv[4][2] = {0, 1, 0, -1, 1, 0, -1, 0};
int mp[MAXN][MAXN], belong[MAXN][MAXN];
bool vis[MAXN][MAXN];

map <int, bool> visedge[MAXL];

struct Node{
    int siz;
    int col;
}node[MAXL];

vector <int> G[MAXL];

void dfs(int x, int y, int tp){
    belong[x][y] = cnt;
    node[cnt].siz++;

    for (int i=0; i<4; i++){
        int nx = x+mv[i][0];
        int ny = y+mv[i][1];

        if (nx<1 || ny<1 || nx>N || ny>N){
            continue;
        }
        if (mp[nx][ny] != tp || vis[nx][ny] == true){
            continue;
        }

        vis[nx][ny] = true;
        dfs(nx, ny, tp);
    }
}

vector <int> colors[MAXL];
map <int, bool> vis_2[MAXL];

void dfs_2(int nod, int col1, int col2){
    recentsiz += node[nod].siz;
    for (int i=0; i<G[nod].size(); i++){
        if (node[G[nod][i]].col == col1 && vis_2[G[nod][i]][col2] == false){
            vis_2[G[nod][i]][col2] = true;
            dfs_2(G[nod][i], col1, col2);       
        }
        else if (node[G[nod][i]].col == col2 && vis_2[G[nod][i]][col1] == false){
            vis_2[G[nod][i]][col1] = true;
            dfs_2(G[nod][i], col1, col2);   
        }
    }   
}

int main(){
    scanf("%d", &N);

    for (int i=1; i<=N; i++){
        for (int j=1; j<=N; j++){
            scanf("%d", &mp[i][j]);
        }
    }

    for (int i=1; i<=N; i++){
        for (int j=1; j<=N; j++){
            if (vis[i][j] == false){
                cnt++;
                node[cnt].col = mp[i][j];
                vis[i][j] = true;
                dfs(i, j, mp[i][j]);
            }   
        }
    }

    for (int i=1; i<=cnt; i++){
        ans1 = max(ans1, node[i].siz);
    }

    int i, j, k;
    for (i=1; i<=N; i++){
        for (j=1; j<=N; j++){
            for (k=0; k<4; k++){
                int nx = i+mv[k][0];
                int ny = j+mv[k][1];

                if (nx<1 || ny<1 || nx>N || ny>N){
                    continue;
                }
                if (visedge[belong[nx][ny]][belong[i][j]] == false){
                    visedge[belong[nx][ny]][belong[i][j]] = true;
                    visedge[belong[i][j]][belong[nx][ny]] = true;               

                    G[belong[nx][ny]].push_back(belong[i][j]);
                    G[belong[i][j]].push_back(belong[nx][ny]);
                    colors[belong[nx][ny]].push_back(node[belong[i][j]].col);
                    colors[belong[i][j]].push_back(node[belong[nx][ny]].col);
                }
            }
        }
    }

    for (int i=1; i<=cnt; i++){
        for (int j=0; j<colors[i].size(); j++){
            if (vis_2[i][colors[i][j]] == false){
                vis_2[i][colors[i][j]] = true;
                recentsiz = 0;
                dfs_2(i, node[i].col, colors[i][j]);
                ans2 = max(ans2, recentsiz);
            }   
        }
    }

    printf("%d\n%d", ans1, ans2);
    return 0;
}

2024.7.15 训练记录

P3034 USACO - Cow Photography

P3034

看到题,第一眼考虑将五次拍照的序列经过一些方法组合得到原序列,思考后发现不好做;我们考虑不变量,每次有一个元素随意移动到新的位置,对于序列元素的绝对位置其实不怎么好刻画,因为可能会产生整体偏移;但仔细一想,这一操作对序列元素的 相对位置 影响不大,因此我们考虑序列元素的相对位置关系

思路已经有了,问题来了,怎么确定要考察的两个元素不是在该次移动的元素呢?题目其实给了一个很重要的条件 —— 一个元素最多只会移动一次,因此我们从整体考虑,对于序列中的两元素 \(a_i, a_j (i<j)\),会对其相对位置造成影响的移动 最多只会出现 \(2\),即将 \(a_j\) 挪到 \(a_i\) 前或将 \(a_i\) 挪到 \(a_j\) 后,题目刚好给了 \(5\) 次拍照结果,因此我们考察元素 \(a_i, a_j\),如果有 \(3\) 次拍照结果中都有 \(i<j\),那么在原序列中就有 \(i<j\)

由此知道了原序列中任意两元素的相对位置关系,现在我们希望求出原序列,正好发现 \(sort\)\(cmp\) 函数可以完美切合这个问题,因此直接任取一次拍照结果 \(sort\) 一遍,排序结果就是原序列

实现细节上,我们要记录在第 \(i(1\le i \le 5)\) 次拍照中元素 \(a_i (0 \le a_i \le 1000000000)\) 的位置,值域很大,因此使用 \(map\) 记录,开 \(O2\) 即可水过;事实上由于本题中没有查询,使用 \(umap\) 会更快,实测不开 \(O2\) 最慢点约 \(700ms\),可过

时间复杂度 \(O(nlogn)\)

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 2e4+5;

int N;
int A[6][MAXN];
int ans[MAXN];

unordered_map <int, unordered_map <int, int> > pos;

bool cmp(int a, int b){
    int cnt=0;
    for (int i=1; i<=5; i++){
        if (pos[i][a] < pos[i][b]){
            cnt++;
        }
    }
    if (cnt >= 3){
        return true;
    }
    return false;
}

int main(){
    scanf("%d", &N);

    for (int i=1; i<=5; i++){
        for (int j=1; j<=N; j++){
            scanf("%d", &A[i][j]);
            pos[i][A[i][j]] = j;
        }
    }
    for (int i=1; i<=N; i++){
        ans[i] = A[1][i];
    }

    sort(ans+1, ans+N+1, cmp);

    for (int i=1; i<=N; i++){
        printf("%d\n", ans[i]);
    }
    return 0;
}

P4188 USACO - Lifeguards S

P4188

我们考虑先求出所有区间覆盖的 时间点,再减去单个区间中与其他区间 不重叠 时间点的数量最小值;注意这里是时间点,所以如果给定区间 \(l, r (l \le r)\),实际上覆盖的时间点有 \(r-l\) 个而非 \(r-l+1\)

先考虑如何求出所有区间覆盖的时间点。首先必不可少的肯定是先给区间按照左端点 \(sort\) 一遍;接下来使用类似双指针的思想,考虑新加入线段 \(line[i]\),参照下图,我们定义 \(borderr\) 表示新加入线段前 己经加入的线段能够覆盖的最大时间点\(line[i].l\) 表示目前区间的左端时间点,\(line[i].r\) 表示目前区间的右端时间点

定义图片

容易发现,如果 \(line[i].r \le borderr\),那么当前线段没有真正产生贡献,跳过即可;当 \(line[i].r > borderr\) 时,我们希望求出当前线段 左侧不与先前加入线段重叠的最大时间点,设为 \(recentl\),这样该线段的贡献即为 \(line[i].r - recentl\),如下图为两种不同情况,易得 \(recentl = max(borderr, line[i].l)\)

总覆盖图片

因此枚举每个区间统计即可,别忘了每次统计后还要更新 \(borderr\),直接令 \(borderr = line[i].r\) 即可,这样我们就解决了总贡献

接下来考虑如何求出单个区间的贡献。设当前区间为 \(line[i]\),首先仍然要特判 \(line[i].r \le borderr\),这说明有区间被完全包含,此时 \(break\) 后直接输出总贡献即可;最坏情况下,单个区间 \(line[i]\) 可能会与之前的区间产生重叠,也会与后一个区间 \(line[i+1]\) 产生重叠,我们考虑 \(line[i].l\)\(borderr\)\(line[i].r\)\(line[i+1].l\) 的关系,左端点处最大无重叠时间点即为 \(max(line[i].l, borderr)\),右端点最大无重叠时间点即为 \(min(line[i].r, line[i+1].l)\),可以结合下图理解

单个区间贡献图片

因此我们统计处最小的单个区间贡献,最终用总贡献减去即可得到最终答案;实现时注意该贡献可能为 负数,此时可以特判为 \(0\) 并直接 \(break\);同时,和统计总贡献时相同,也别忘了更新 \(borderr\)\(borderr = line[i].r\)

时间复杂度 \(O(nlogn)\)

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e5+5;

int N;
long long sum, mcover=1e9;

struct Line{
    int l;
    int r;
}line[MAXN];

bool cmp(Line a, Line b){
    if (a.l == b.l){
        return a.r < b.r;
    }
    return a.l < b.l;
}

int main(){
    scanf("%d", &N);

    for (int i=1; i<=N; i++){
        scanf("%d%d", &line[i].l, &line[i].r);
    }
    sort(line+1, line+N+1, cmp);

    int borderr = 0;
    for (int i=1; i<=N; i++){
        if (line[i].r > borderr){
            int recentl = max(line[i].l, borderr);
            sum += line[i].r-recentl;
            borderr = line[i].r;
        }   
    }

    borderr = 0;
    line[N+1].l = line[N].r;
    for (int i=1; i<=N; i++){
        if (line[i].r <= borderr){
            mcover = 0;
        }
        else{
            long long cover = min(line[i+1].l, line[i].r)-max(line[i].l, borderr);
            mcover = min(mcover, cover);
            borderr = line[i].r;
        }

        if (mcover <= 0){
            mcover = 0;
            break;
        }   
    }

    printf("%lld", sum-mcover);
    return 0;
}

P4181 USACO - Rental Service S

P4181

每个奶牛有两种分配方式,一种是将奶牛租出去,一种是卖出该奶牛生产的奶;首先注意到租奶牛这一方式对具体租哪一只奶牛没有限制,因此易得贪心,即 优先租产奶量小的奶牛,同时 优先租给价格最高的农场主;同理,卖奶时也 优先卖给价格最高的商店

因此,我们将商店按照价格 从大到小排序、将租奶牛的农场主按照租用价格 从大到小排序;我们注意到将所有奶牛排序后,必然有从头连续一部分选择租出去,剩下一部分选择卖出去,因此考虑 枚举断点 \(i\) ,钦定将第 \(1\) 到第 \(i\) 头奶牛的奶卖出去,第 \(i+1\) 到第 \(N\) 头奶牛租出去;所以,我们也要对奶牛按照产奶量 从大到小排序

这样,我们将总贡献分为两部分,一部分来自租出奶牛;这一部分很好维护,直接上 前缀和 即可,对排序后的 \(r[i]\) 数组,定义 \(sum[i]\) 为其前缀和数组,表示租出 \(i\) 头牛能得到的钱

对于卖奶牛奶的部分,虽然卖出奶的总和也可以用前缀和维护,但如果每次都从第 \(i\) 个商店向后循环处理是否能卖出奶会超时;由于我们知道相邻两次卖奶总和的 增量 (设当前奶牛为 \(i\),这个增量实际上就是 \(c[i]\)),因此可以 直接考虑其对答案的影响,记录下上一次卖完后所得所有的钱 \(sell\),当前卖到的商店位置 \(cnt\),直接进行处理即可;这里有个技巧,我们可以单独开一个变量记录当前商店中已有的牛奶 \(storemilk\),这样省去了直接改动商店原本信息的麻烦

具体的,设当前要卖出的牛奶 (增量) 为 \(milk=c[i]\),第 \(i\) 个的商店的容量为 \(store[i].q\) (实际剩余容量为 \(store[i].q-storemilk\)) 、价格为 \(store[i].p\),分情况讨论;

  1. 当商店剩余容量不足 \(milk\)
    • \(store[i].q-storemilk\) 更新 \(sell\)
  2. 更新当前要卖出的牛奶数量 \(milk\)
  3. 收回 \(storemilk\),将其设为 \(0\)
  4. 别忘了进行 \(cnt+1\)
  5. 当商店剩余容量够 \(milk\)
  6. \(milk\) 更新 \(sell\) (特别注意!这里不是 \(storemilk+milk\))
  7. 更新 \(storemilk\) (因为这里 \(store[i].q\) 的变化实际上直接体现在了 \(storemilk\) 的变化上)
  8. 跳出循环即可,因为已经卖光了

最终统计出当前的总价值,加上租出的价值,与最终答案 \(ans\) 比较,这里如果 当前总价值 $ \le ans$,其实 可以直接 \(break\) ,因为容易证明价值变化分界点唯一

代码

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e5+5;

int N, M, R;
int c[MAXN], r[MAXN];
long long sumr[MAXN];
long long ans = 0;

struct Store{
    int p;
    int q;
}store[MAXN];

bool cmp(int a, int b){
    return a>b;
}
bool cmp_store(Store a, Store b){
    if (a.p == b.p){
        return a.q > b.q;
    }
    return a.p > b.p;
}

int main(){
    scanf("%d%d%d", &N, &M, &R);

    for (int i=1; i<=N; i++){
        scanf("%d", &c[i]);
    }
    sort(c+1, c+N+1, cmp);
    for (int i=1; i<=M; i++){
        scanf("%d%d", &store[i].q, &store[i].p);
    }
    sort(store+1, store+M+1, cmp_store);
    for (int i=1; i<=R; i++){
        scanf("%d", &r[i]);
    }
    sort(r+1, r+R+1, cmp);

    for (int i=1; i<=R; i++){
        sumr[i] = sumr[i-1]+r[i];   
    }

    long long storemilk=0, cnt=1;
    long long sell=0, rent=0;

    for (int i=0; i<=N; i++){
        long long milk = c[i];
        while (cnt<=M){
            if (milk+storemilk >= store[cnt].q){
                milk = milk+storemilk-store[cnt].q;
                sell += 1ll*(store[cnt].q-storemilk)*store[cnt].p;
                storemilk = 0;
                cnt++;
            }   
            else{
                sell += 1ll*milk*store[cnt].p;
                storemilk = milk+storemilk;
                break;
            }
        }   
        rent = sumr[min(N-i, R)];
        if (rent+sell > ans){
            ans = rent+sell;
        }
        else{
            break;
        }
    }

    printf("%lld", ans);
    return 0;
}

P4265 - USACO Snow Boots S

P4265

考虑 \(DP\);设 \(dp[i]\) 表示到第 \(i\) 格所穿的靴子编号 (也就是丢弃了 \(dp[i]-1\) 双靴子),考虑转移,显然到第 \(i\) 格只能由前面的格子转移而来,因此枚举 \(j (1 \le j < i)\);因为我们要么直接穿着在第 \(j\) 格的靴子走到第 \(i\) 格,要么在第 \(j\) 格脱下一些靴子再走到第 \(i\) 格,所以再枚举 \(k (dp[j] \le k \le B)\),表示到达第 \(i\) 格时所穿的靴子

我们想从第 \(j\) 格穿着第 \(k\) 个靴子走到第 \(i\) 格,需要: - 第 \(k\) 个靴子能够走在第 \(j\) 格的雪上 - 第 \(k\) 个靴子能够走在第 \(i\) 格的雪上 - 第 \(k\) 个靴子能走的距离至少是 \(i\)\(j\) 的距离

转移方程很简单,就是 \(dp[i] = min(dp[i], k)\),答案即为 \(dp[N]-1\),注意不要忘记 \(-1\)

初始情况为 \(dp[1] = 0\),因为一开始不穿靴子,另外由于需要求最小值,别忘了在最开始 \(memset\) 一下

时间复杂度 \(O(N^2B)\)

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 305;

int N, B;
int h[MAXN], s[MAXN], d[MAXN];
int dp[MAXN];

int main(){
    scanf("%d%d", &N, &B);

    for (int i=1; i<=N; i++){
        scanf("%d", &h[i]);
    }
    for (int i=1; i<=B; i++){
        scanf("%d%d", &s[i], &d[i]);
    }

    memset(dp, 0x3f, sizeof(dp));
    dp[1] = 0;
    for (int i=2; i<=N; i++){
        for (int j=1; j<i; j++){
            for (int k=dp[j]; k<=B; k++){
                if (s[k] >= h[i] && s[k] >= h[j] && d[k]>=i-j){
                    dp[i] = min(dp[i], k);
                }
            }   
        }
    }

    printf("%d", dp[N]-1);
    return 0;
}

P4264 USACO - Teleportation S

P4264

不会。咕咕咕。