基础算法 - 总结与练习

位运算

【例题】飞行员兄弟

“飞行员兄弟” 这个游戏,需要玩家顺利的打开一个拥有 16 16 16 个把手的冰箱。

已知每个把手可以处于以下两种状态之一:打开或关闭。

只有当所有把手都打开时,冰箱才会打开。

把手可以表示为一个 4 × 4 4\times4 4×4 的矩阵,您可以改变任何一个位置 [ i , j ] [i,j] [i,j] 上把手的状态。

但是,这也会使得第 i i i 行和第 j j j 列上的所有把手的状态也随着改变。

请你求出打开冰箱所需的切换把手的次数最小值是多少。

1 ≤ i , j ≤ 4 1\le i,j\le4 1i,j4

分析:

这个题目就是典型的 01 01 01 矩阵点击问题,所以每个位置上只有按与不按的区别,所以对于 4 × 4 4\times 4 4×4 的矩阵而言,一共就只有 2 4 2^4 24 种操作方案,我们使用二进制枚举就可以遍历完所有的方案,然后在这些方案中找最大值就行了。

代码如下:

#include <bits/stdc++.h>
using namespace std;
char g[5][5], backup[5][5];

void turn(int x, int y) {
    for(int i = 0; i < 4; ++i) {
        if(i != x) {
            g[i][y] = (g[i][y] == '-' ? '+' : '-');
        }
        
        if(i != y) {
            g[x][i] = (g[x][i] == '-' ? '+' : '-');
        }
    }
    
    g[x][y] = (g[x][y] == '-' ? '+' : '-');
}

int main()
{
    for(int i = 0; i < 4; ++i)
        for(int j = 0; j < 4; ++j)
            cin >> g[i][j];
    
    int res = 0x3f3f3f3f, res_way = -1;
    
    memcpy(backup, g, sizeof g);
    
    for (int way = 0; way < 1 << 16; way ++ ) {
        
        int cnt = 0;
        
        for(int i = 0; i < 16; ++i) if(way >> i & 1) {
            int x = i / 4, y = i % 4;
            turn(x, y);
            cnt++;
        }
        
        bool flag = true;
        
        for (int i = 0; i < 4; i ++ ) {
            
            for(int j = 0; j < 4; ++j) if(g[i][j] != '-') {
                flag = false;
                break;
            }
                
            if(!flag) break; 
            
        }
        
        if(flag && cnt < res) {
            res_way = way;
            res = cnt;
        }
        
        memcpy(g, backup, sizeof backup);
        
    }
    
    cout << res << endl;
    
    for(int i = 0; i < 16; ++i) if(res_way >> i & 1) {
        int x = i / 4, y = i % 4;
        cout << x + 1 << ' ' << y + 1 << endl;
    }
    
    return 0;
}

模拟

【例题】占卜DIY

达达学会了使用扑克 D I Y DIY DIY 占卜。

方法如下:

一副去掉大小王的扑克共 52 52 52 张,打乱后均分为 13 13 13 堆,编号 1 1 1 13 13 13,每堆 4 4 4 张,其中第 13 13 13 堆称作 “生命牌” ,也就是说你有 4 4 4 条命。

这里边, 4 4 4 K K K 被称作死神。

初始状态下,所有的牌背面朝上扣下。

流程如下:

  1. 抽取生命牌中的最上面一张(第一张)。

  2. 把这张牌翻开,正面朝上,放到牌上的数字所对应编号的堆的最上边。(例如抽到 2 2 2,正面朝上放到第 2 2 2 堆牌最上面,又比如抽到 J J J,放到第 11 11 11 堆牌最上边,注意是正面朝上放)

  3. 从刚放了牌的那一堆最底下(最后一张)抽取一张牌,重复第 2 2 2 步。(例如你上次抽了 2 2 2,放到了第二堆顶部,现在抽第二堆最后一张发现是 8 8 8,又放到第 8 8 8 堆顶部…)

  4. 在抽牌过程中如果抽到 K K K,则称死了一条命,就扔掉 K K K 再从第 1 1 1 步开始。

  5. 当发现四条命都死了以后,统计现在每堆牌上边正面朝上的牌的数目,只要同一数字的牌出现 4 4 4 张正面朝上的牌(比如 4 4 4 A A A),则称 “开了一对” ,当然 4 4 4 K K K 是不算的。

  6. 统计一共开了多少对,开了 0 0 0 对称作 ”极凶” , 1 1 1 2 2 2 对为 “大凶” , 3 3 3 对为 “凶” , 4 4 4 5 5 5 对为 “小凶” , 6 6 6 对为 “中庸” , 7 7 7 8 8 8 对 “小吉” , 9 9 9 对为 “吉” , 10 10 10 11 11 11 为 “大吉” , 12 12 12 为 “满堂开花,极吉” 。

统计得到的开出的总对数。

分析:

对于模拟题,一定要知道模拟的全部过程,如果有必要的话,可以把流程写到草稿纸上。知道了模拟过程后,还要确定好怎么去写,恰当的写法可以带来避免很多调试。

代码如下:

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

vector<int> down[15];
int up[15];

inline int get_num() {
    char ch;
    cin >> ch;
    
    if(isdigit(ch)) {
        if(ch == '0') return 10;
        return ch - '0';
    } else {
        if(ch == 'A') return 1;
        if(ch == 'J') return 11;
        if(ch == 'Q') return 12;
        if(ch == 'K') return 13;
    }
}

int main()
{

    for(int i = 1; i <= 13; ++i) {
        int x;
        for(int j = 0; j < 4; ++j) {
            x = get_num();
            down[i].push_back(x);
        }
    }
    
    for(int i = 0; i < 4; ++i) {
        int num = down[13][i];
        
        while(num != 13) {
            up[num] ++ ;
            int t = num;
            num = down[num].back();
            down[t].pop_back();
        }
        
    }
    
    int res = 0;
    for(int i = 1; i <= 12;  ++i) {
        res += up[i] == 4 ? 1 : 0;
    }
    
    cout << res ;
    
    return 0;
}

递归

【例题】分形

分形,具有以非整数维形式充填空间的形态特征。

通常被定义为 “一个粗糙或零碎的几何形状,可以分成数个部分,且每一部分都(至少近似地)是整体缩小后的形状” ,即具有自相似的性质。

现在,定义 “盒子分形” 如下:

一级盒子分形:

   X

二级盒子分形:

   X X
    X
   X X

如果用 B ( n − 1 ) B(n−1) B(n1) 代表第 n − 1 n−1 n1 级盒子分形,那么第 n n n 级盒子分形即为:

  B(n - 1)        B(n - 1)

          B(n - 1)

  B(n - 1)        B(n - 1)

你的任务是绘制一个 n n n 级的盒子分形。

n ≤ 7 n \le 7 n7

分析:

对于分形基本上是利用递归处理的,对于这道题,我们设计递归状态 ( x , x 1 , y 1 , x 2 , y 2 ) (x,x1,y1,x2,y2) (x,x1,y1,x2,y2) 表示为当前是在左上角坐标 ( x 1 , y 1 ) (x1,y1) (x1,y1)、右上角坐标 ( x 2 , y 2 ) (x2,y2) (x2,y2) x x x 级盒子分形,那么它是由 5 5 5 x − 1 x - 1 x1 级盒子分形组成的,而这 5 5 5 个盒子分形的位置分别是:

  • ( x − 1 , x 1 , y 1 , x 1 + l e n − 1 , y 1 + l e n − 1 ) (x - 1, x1, y1, x1 + len - 1, y1 + len - 1) (x1,x1,y1,x1+len1,y1+len1) 左上角盒子分形
  • ( x − 1 , x 1 + l e n , y 1 + l e n , x 2 − l e n , y 2 − l e n ) (x - 1, x1 + len, y1 + len, x2 - len, y2 - len) (x1,x1+len,y1+len,x2len,y2len) 中间盒子分形
  • ( x − 1 , x 2 − l e n + 1 , y 2 − l e n + 1 , x 2 , y 2 ) (x - 1, x2 - len + 1, y2 - len + 1, x2, y2) (x1,x2len+1,y2len+1,x2,y2) 右下角盒子分形
  • ( x − 1 , x 2 − l e n + 1 , y 1 , x 2 , y 1 + l e n − 1 ) (x - 1, x2 - len + 1, y1, x2, y1 + len - 1) (x1,x2len+1,y1,x2,y1+len1) 左下角盒子分形
  • ( x − 1 , x 1 , y 2 − l e n + 1 , x 1 + l e n − 1 , y 2 ) (x - 1, x1, y2 - len + 1, x1 + len - 1, y2) (x1,x1,y2len+1,x1+len1,y2) 右上角盒子分形

x x x 1 1 1 时,就代表 ( x 1 , y 1 ) (x1, y1) (x1,y1) 处填上一个 X X X

不过这道题坑点是每个测试点间要输出 - 来分隔。

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int n;
char g[N][N];

int qpow(int a, int b) {
    int res = 1;
    while(b) {
        if(b & 1) res *= a;
        a *= a;
        b >>= 1;
    }
    return res;
}

void build(int x, int x1, int y1, int x2, int y2) {
    if(x == 1) {
        g[x1][y1] = 'X';
        return ;
    }
    int len = qpow(3, x - 2);

    build(x - 1, x1, y1, x1 + len - 1, y1 + len - 1);
    build(x - 1, x1 + len, y1 + len, x2 - len, y2 - len);
    build(x - 1, x2 - len + 1, y2 - len + 1, x2, y2);

    build(x - 1, x2 - len + 1, y1, x2, y1 + len - 1);
    build(x - 1, x1, y2 - len + 1, x1 + len - 1, y2);

}


int main()
{
    build(7, 0, 0, qpow(3, 7 - 1) - 1, qpow(3, 7 - 1) - 1);

    while(scanf("%d", &n) && ~n) {
        int len = qpow(3, n - 1);
        for(int i = 0; i < len; ++i){
            for(int j = 0; j < len; ++j)
                if(g[i][j] == 'X') cout << g[i][j];
                else cout << ' ';
            cout << endl;
        }
        puts("-");
    }
    return 0;
}

分治

【例题】袭击

在与联盟的战斗中屡战屡败后,帝国撤退到了最后一个据点。

依靠其强大的防御系统,帝国击退了联盟的六波猛烈进攻。

经过几天的苦思冥想,联盟将军亚瑟终于注意到帝国防御系统唯一的弱点就是能源供应。

该系统由 N N N 个核电站供应能源,其中任何一个被摧毁都会使防御系统失效。

将军派出了 N N N 个特工进入据点之中,打算对能源站展开一次突袭。

不幸的是,由于受到了帝国空军的袭击,他们未能降落在预期位置。

作为一名经验丰富的将军,亚瑟很快意识到他需要重新安排突袭计划。

他现在最想知道的事情就是哪个特工距离其中任意一个发电站的距离最短。

你能帮他算出来这最短的距离是多少吗?

1 ≤ N ≤ 100000 , 0 ≤ X , Y ≤ 1000000000 1\le N\le100000,\\ 0\le X,Y \le1000000000 1N100000,0X,Y1000000000

分析:

这个是经典的最近点对问题,不过是拓展版的。经典的最近点点对问题就是在平面上有若干个点,找出最近两点的距离。这是典型的分治问题,算法步骤如下:

  1. 对坐标按照 x x x 坐标排序。
  2. 解决问题 ( 0 , n ) (0, n) (0,n) ,表示从第一个点到第 n n n 点这里面最短距离。
  3. 问题递归为 ( 0 , m i d ) (0,mid) (0,mid) ( m i d + 1 , n ) (mid + 1, n) (mid+1,n)
  4. 所以问题 ( 0 , n ) (0,n) (0,n) 的解就是 问题 ( 0 , m i d ) (0,mid) (0,mid) ( m i d + 1 , n ) (mid + 1, n) (mid+1,n) m i d _ x mid\_x mid_x 附近点三者的最小值。

这里面最难处理的就是中间部分了,这里我借用一下其他人的图片,图片是抽风大佬的图。他的题解
在这里插入图片描述
我们以 m i d mid mid 这根线递归下去了两部分,显然我们在这一层递归已经知道了问题 ( l e f t , m i d ) (left,mid) (left,mid) ( m i d + 1 , r i g h t ) (mid + 1,right) (mid+1,right) 的解,那么在考虑中间点附近时就可以通过两个值来缩小范围。

在这里插入图片描述
那么如果 x x x 轴坐标里 m i d mid mid 的距离都大于了 a n s ans ans 则就不用考虑了,这样我们缩小的考虑范围了,对于这里面的点我们怎么快速的算呢?我们先看最坏考虑情况所需要的考虑的点数:

在这里插入图片描述
我们划分了 6 6 6 个点,为什么是 6 6 6 个点呢?假设这个范围有七个点,那么就会出现下面这种点对距离。

在这里插入图片描述
蓝色的线就是两点的距离,通过距离公式可以算出为 5 6 r < r = a n s \frac{5}{6}r < r = ans 65r<r=ans ,显然就与之前的 a n s ans ans 违背了。故不能存在 6 6 6 个点以上,那么我们就可以放心使用循环去遍历这些点,第二层循环最多循环 6 6 6 次就结束。

剩下的细节直接通过代码呈现吧。当然这是最近点对只有一种的情况,题目是两种点间的最短距离,所以我们可以在求距离时,避开求相同类型的,这就可以了。

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N = 200010;
const double INF = 1e10; 

struct Point {
    double x, y;
    int type;

    bool operator < (const Point &_x) const {
        return x < _x.x;
    }

} points[N], temp[N];

int n;

double dist(Point a, Point b) {
    double dx = (a.x - b.x), dy = (a.y - b.y); 
    return sqrt(dx * dx + dy * dy);
}

double dfs(int l, int r) {
    if(l >= r) return INF;
    int mid = (l + r) >> 1;

    double mid_x = points[mid].x;
    double res = min(dfs(l, mid), dfs(mid + 1, r));


    {
        int i = l, j = mid + 1, k = 0;
        while(i <= mid && j <= r) 
            if(points[i].y <= points[j].y) temp[k++] = points[i++]; 
            else temp[k++] = points[j++];

        while(i <= mid) temp[k++] = points[i++];
        while(j <= r) temp[k++] = points[j++];

        for(i = l, j = 0; i <= r; ++i) 
            points[i] = temp[j++];
    }

    int k = 0;
    for(int i = l; i <= r; ++i) 
        if(points[i].x >= mid_x - res && points[i].x <= res + mid_x)
            temp[k++] = points[i];

    for(int i = 0; i < k; ++i) {
        for(int j = i - 1; j >= 0 && temp[i].y - temp[j].y < res && temp[i].type != temp[j].type; --j) {
            res = min(res, dist(temp[i], temp[j]));
        }
    }

    return res;
}


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

    while (t -- ) {
        scanf("%d", &n);

        for(int i = 0; i < n; ++i) {
            scanf("%lf%lf", &points[i].x, &points[i].y);
            points[i].type = 0;

        }

        for(int i = n; i < n << 1; ++i) {
            scanf("%lf%lf", &points[i].x, &points[i].y);
            points[i].type = 1;

        }

        sort(points, points + (n << 1));

        double res = dfs(0, (n << 1) - 1);

        printf("%.3f\n", res);

    }

    return 0;
}

二分

【例题】防线

达达学习数学竞赛的时候受尽了同仁们的鄙视,终于有一天…受尽屈辱的达达黑化成为了黑暗英雄怪兽达达。

就如同中二漫画的情节一样,怪兽达达打算毁掉这个世界。

数学竞赛界的精英 lqr 打算阻止怪兽达达的阴谋,于是她集合了一支由数学竞赛选手组成的超级行动队。

由于队员们个个都智商超群,很快,行动队便来到了怪兽达达的黑暗城堡的下方。

但是,同样强大的怪兽达达在城堡周围布置了一条“不可越过”的坚固防线。

防线由很多防具组成,这些防具分成了 N N N 组。

我们可以认为防线是一维的,那么每一组防具都分布在防线的某一段上,并且同一组防具是等距离排列的。

也就是说,我们可以用三个整数 S S S E E E D D D 来描述一组防具,即这一组防具布置在防线的 S , S + D , S + 2 D , ⋯   , S + K D ( K ∈ Z , S + K D ≤ E , S + ( K + 1 ) D > E ) S,S+D,S+2D,\cdots,S+KD(K\in Z,S+KD\le E,S+(K+1)D>E) S,S+D,S+2D,,S+KD(KZS+KDES+(K+1)D>E)位置上。

黑化的怪兽达达设计的防线极其精良。

如果防线的某个位置有偶数个防具,那么这个位置就是毫无破绽的(包括这个位置一个防具也没有的情况,因为 0 0 0 也是偶数)。

只有有奇数个防具的位置有破绽,但是整条防线上也最多只有一个位置有奇数个防具。

作为行动队的队长,lqr 要找到防线的破绽以策划下一步的行动。

但是,由于防具的数量太多,她实在是不能看出哪里有破绽。

作为 lqr 可以信任的学弟学妹们,你们要帮助她解决这个问题。

防具总数不多于 1 0 8 10^8 108 S i ≤ E i S_i\le E_i SiEi 1 ≤ T ≤ 5 1\le T\le 5 1T5
N ≤ 200000 , 0 ≤ S i , E i , D i ≤ 2 31 − 1 N\le200000, 0\le S_i,E_i,D_i\le2^{31}−1 N200000,0Si,Ei,Di2311

分析:

这道题目的关键点为奇数位置点的个数最多为一个,那么奇数点的左边就全为偶数点、右边也全为偶数点,使用二分就是去看题目有哪些性质是单调性可以使用,而奇数点的位置就具有单调性,如果当前二分点的左边所有点总和为偶数,那么奇数点肯定在右边,反之在左边,然后利用这样的单调性就可以以 O ( n   l o g ( n ) ) O(n~log(n)) O(n log(n)) 时间复杂度来找出它。

代码如下:

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e5 + 5;

int n; 
LL l[N], r[N], d[N];

int check(LL _x) {
    int res = 0;
    for(int i = 0; i < n; ++i) {
        if(_x >= l[i])
            res += (min(r[i], _x) - l[i]) / d[i] + 1;
    }
    return res;
}

int main()
{
    int t;
    scanf("%d", &t);
    while(t -- ) {
        scanf("%d", &n);
        
        LL L = (1 << 31) - 1, R = 0;
        
        for(int i = 0 ; i < n; ++i) {
            scanf("%lld%lld%lld", &l[i], &r[i], &d[i]);
            L = min(l[i], L);
            R = max(r[i], R);
        }
        
        while(L < R) {
            LL mid = (L + R) >> 1;
            if(check(mid) & 1) R = mid;
            else L = mid + 1;
        }
        
        int res = check(R) - check(R - 1);
        
        if(res & 1) printf("%lld %d\n", L, res);
        else puts("There's no weakness.");
    }
    return 0;
}

【例题】赶牛出圈

农夫约翰希望为他的奶牛们建立一个畜栏。

这些挑剔的畜生要求畜栏必须是正方形的,而且至少要包含 C C C 单位的三叶草,来当做它们的下午茶。

畜栏的边缘必须与 X X X Y Y Y 轴平行。

约翰的土地里一共包含 N N N 单位的三叶草,每单位三叶草位于一个 1 × 1 1\times1 1×1 的土地区域内,区域位置由其左下角坐标表示,并且区域左下角的 X , Y X,Y X,Y 坐标都为整数,范围在 1 1 1 10000 10000 10000 以内。

多个单位的三叶草可能会位于同一个 1 × 1 1\times1 1×1 的区域内,因为这个原因,在接下来的输入中,同一个区域坐标可能出现多次。

只有一个区域完全位于修好的畜栏之中,才认为这个区域内的三叶草在畜栏之中。

请你帮约翰计算一下,能包含至少 C C C 单位面积三叶草的情况下,畜栏的最小边长是多少。

1 ≤ C ≤ 500 , C ≤ N ≤ 500 1\le C\le500 ,\\ C\le N\le500 1C500,CN500

分析:

其实这道题想到二分、前缀和、离散化在学了这些后都能想到的,不过难在可枚举长度为 l e n len len 的正方形上,如何把 n 4 n^4 n4 的复杂度降到 n 2 n^2 n2 上面。

首先二分长度然后使用判定长度是否符合要求,但是 X , Y X,Y X,Y 的范围有 10000 10000 10000 所以不可能直接使用前缀和,但是好在 n n n 的范围不大只有 500 500 500 ,所以可以使用离散化来把数值映射到 1 1 1 ~ 500 500 500 以内,然后使用前缀和就行了。

之后就是判定问题了,因为长度为 l e n len len 所以使用了滑动窗口,使得原本是 n 4 n^4 n4 的变为 n 2 n^2 n2

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N = 505;

int dis[N * 2], x[N], y[N];
int sa[N * 2][N * 2];

int c, n, cnt;

int query(int _x) {
    return lower_bound(dis + 1 , dis + 1 + cnt, _x) - dis;
}

bool check(int len) {
    for(int x1 = 0, x2 = 1; x2 <= cnt; ++x2) {
        while(dis[x2] - dis[x1 + 1] + 1 > len) x1++;
        for(int y1 = 0, y2 = 1; y2 <= cnt; ++y2) {
            while(dis[y2] - dis[y1 + 1] + 1 > len) y1++;
            if(sa[x2][y2] - sa[x2][y1] - sa[x1][y2] + sa[x1][y1] >= c) {
                return true;
            }
        }
    }
    return false;
}

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

    for(int i = 0; i < n; ++i) {
        scanf("%d%d", x + i, y + i);
        dis[++cnt] = x[i];
        dis[++cnt] = y[i];
    }

    sort(dis + 1, dis + 1 + cnt);
    cnt = unique(dis + 1, dis + 1 + cnt) - dis;
    cnt -- ;

    for(int i = 0, _x, _y; i < n; ++i) {
        _x = query(x[i]);
        _y = query(y[i]);

        sa[_x][_y] ++ ;
    }

    for(int i = 1; i <= cnt; ++i) {
        for(int j = 1; j <= cnt; ++j)
            sa[i][j] += sa[i - 1][j] + sa[i][j - 1] - sa[i - 1][j - 1];
    }

    int l = 1, r = 10000;

    while(l < r) {
        int mid = (l + r) >> 1;
        if(check(mid)) r= mid;
        else l = mid + 1;
    }

    printf("%d", l);

    return 0;
}

排序

【例题】糖果传递

n n n 个小朋友坐成一圈,每人有 a [ i ] a[i] a[i] 个糖果。

每人只能给左右两人传递糖果。

每人每次传递一个糖果代价为 1 1 1

求使所有人获得均等糖果的最小代价。

1 ≤ n ≤ 1000000 , 0 ≤ a [ i ] ≤ 2 × 1 0 9 , 1\le n\le1000000 , 0 \le a[i]\le2\times10^9, 1n1000000,0a[i]2×109,
数据保证一定有解。

分析:

做这道题我深刻体会到了题目抽象成数学模型的重要性,题目中的内容就可以抽象成下图:

x1
x2
xn-1
xn
a1
a2
...
an

a [ 1 ] a[1] a[1] 最后失去 x 1 x_1 x1 份糖果,得到 x n x_n xn 份糖果。
a [ 2 ] a[2] a[2] 最后失去 x 2 x_2 x2 份糖果,得到 x 1 x_1 x1 份糖果。

a [ n ] a[n] a[n] 最后失去 x n x_n xn 份糖果,得到 x n − 1 x_{n-1} xn1 份糖果。

最后它们都变成了 a ˉ \bar{a} aˉ
所以
a 1 − x 1 + x n = a ˉ a 2 − x 2 + x 1 = a ˉ . . . a n − 1 − x n − 1 + x n − 2 = a ˉ a n − x n + x n − 1 = a ˉ a_1 - x_1 + x_n = \bar{a} \\ a_2 - x_2 + x_1 = \bar{a} \\ ...\\ a_{n-1} - x_{n-1} + x_{n - 2} = \bar{a} \\ a_{n} - x_{n} + x_{n - 1} = \bar{a} \\ a1x1+xn=aˉa2x2+x1=aˉ...an1xn1+xn2=aˉanxn+xn1=aˉ

因为我们最后要求的是操作数最小,所以等价于 ∣ x 1 ∣ + ∣ x 2 ∣ + ⋯ + ∣ x n ∣ |x_1| + |x_2| + \cdots + |x_n| x1+x2++xn 最小。所以我们再变一下形:
x n = a ˉ − a 1 − x 1 x n − 1 = 2 a ˉ − ( a 1 + a n ) − x 1 x n − 2 = 3 a ˉ − ( a 1 + a n + a n − 1 ) − x 1 . . . x 2 = ( n − 1 ) a ˉ − ( a 1 + a n + a n − 1 + ⋯ + a 3 ) − x 1 x 1 = x 1 x_n = \bar{a} - a_1 - x_1\\ x_{n-1} = 2\bar{a} - (a_1 + a_n) - x_1\\ x_{n-2} = 3\bar{a} - (a_1 + a_n + a_{n-1}) - x_1\\ ... x_2 = (n-1)\bar{a} - (a_1 + a_n + a_{n - 1}+\cdots+a_3) - x_1 \\ x_1 = x_1 \\ xn=aˉa1x1xn1=2aˉ(a1+an)x1xn2=3aˉ(a1+an+an1)x1...x2=(n1)aˉ(a1+an+an1++a3)x1x1=x1

此时,对于 x i x_i xi 而言,它的 ( n − i + 1 ) a ˉ − a 1 − ∑ j = i + 1 n a j (n-i+1)\bar{a}-a_1-\sum_{j = i + 1}^{n}a_j (ni+1)aˉa1j=i+1naj 是个常数,所以等式:
∣ x 1 ∣ + ∣ x 2 ∣ + ⋯ + ∣ x n ∣ = ∣ 0 − x 1 ∣ + ∣ c 1 − x 1 ∣ + ⋯ + ∣ c n − 1 − x 1 ∣ |x_1| + |x_2| + \cdots + |x_n| = |0 - x_1| + |c_1 - x_1| +\cdots+|c_{n-1} - x_1| x1+x2++xn=0x1+c1x1++cn1x1

而这就是当 x 1 x_1 x1 c c c 的中位数时,等式就最小。

代码如下:

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1000010;
int n;
LL c[N], a[N], sum;

int main()
{
    scanf("%d", &n);
    for(int i = 0; i < n; ++i) 
        scanf("%lld", a + i), sum += a[i];

    c[0] = a[0];

    sum /= n;

    for(int i = 1; i < n; ++i)
        c[i] = c[i - 1] + a[i] - sum;

    sort(c, c + n);

    LL res = 0;

    for(int i = 0; i < n; ++i) 
        res += abs(c[i] - c[n - i - 1]);

    printf("%lld", res / 2);

    return 0;
}

【例题】士兵

格格兰郡的 N N N 名士兵随机散落在全郡各地。

格格兰郡中的位置由一对 ( x , y ) (x,y) (x,y) 整数坐标表示。

士兵可以进行移动,每次移动,一名士兵可以向上,向下,向左或向右移动一个单位(因此,他的 x x x y y y 坐标也将加 1 1 1 或减 1 1 1)。

现在希望通过移动士兵,使得所有士兵彼此相邻的处于同一条水平线内,即所有士兵的 y y y 坐标相同并且 x x x 坐标相邻。

请你计算满足要求的情况下,所有士兵的总移动次数最少是多少。

需注意,两个或多个士兵不能占据同一个位置。

1 ≤ N ≤ 10000 , − 10000 ≤ x [ i ] , y [ i ] ≤ 10000 1\le N\le10000,\\ −10000\le x[i],y[i]\le10000 1N10000,10000x[i],y[i]10000

分析:

对于二维题目,通常我们会先尝试是否两个维度是否可以独立,比如这道题显然 x x x y y y 是可以独立的,对于 y y y 轴而言,就是中位数就是最短的距离,对于 x x x 轴就要转换一下了。

对于 x 1 , x 2 , ⋯   , x n x_1,x_2,\cdots,x_n x1,x2,,xn 而言,它们要移动为 x 1 ′ , x 2 ′ , ⋯   , x n ′ x_1',x_2',\cdots,x_n' x1,x2,,xn 的相邻序列,显然移动前后,原有的 x x x 的顺序应该不会变化,即 x i → x i ′ x_i\to x_i' xixi 这样。 所以:
x 1 → x 1 ′ x 2 → x 1 ′ + 1 x 3 → x 1 ′ + 2 . . . x n → x 1 ′ + n − 1 x_1\to x_1'\\ x_2\to x_1' + 1\\ x_3\to x_1' + 2\\ ...\\ x_n\to x_1' + n - 1\\ x1x1x2x1+1x3x1+2...xnx1+n1

那么移动的花费就是:
∣ x 1 − x 1 ′ ∣ + ∣ x 2 − 1 − x 1 ′ − 1 ∣ + ∣ x 3 − 2 − x 1 ′ ∣ + ⋯ + ∣ x n − n + 1 − x 1 ′ ∣ |x_1-x_1'| + |x_2-1 -x_1'-1| + |x_3-2-x_1'| + \cdots + |x_n-n +1-x_1'| x1x1+x21x11+x32x1++xnn+1x1

那么这很显然也是中位数了。

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N = 10010;
int n;
int y[N], x[N];
int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) {
        scanf("%d%d", &x[i], &y[i]);
    }
    
    sort(x, x + n);
    sort(y, y + n);
    
    int res = 0;
    for(int i = 0; i < n; ++i)
        res += abs(y[i] - y[n - i - 1]);
        
    res /= 2;
    
    int ans = 0;
    
    for(int i = 0; i < n; ++i)
        x[i] -= i;
        
    sort(x, x + n);
    
    for(int i = 0; i < n; ++i)
        ans += abs(x[i] - x[n - i - 1]);
    
    ans /= 2;
        
    printf("%d",  ans + res);
    
    return 0;
}

递推

【例题】最大的和

#include <iostream>
using namespace std;
const int N = 103;
const int INF = 0x3f3f3f3f;
int S[N][N];

int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ ) 
            scanf("%d", &S[i][j]);
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            S[i][j] += S[i - 1][j] + S[i][j - 1] - S[i - 1][j - 1];
    
    int res = - INF;
    
    for(int len = 1; len <= n; ++len) {
        for(int i = 1; i + len - 1 <= n; ++i) {
            int j = i + len - 1;
            
            int last = 0;
            
            for(int k = 1; k <= n; ++k) {
                int num = S[j][k] - S[i - 1][k] - S[j][k - 1] + S[i - 1][k - 1];
                
                if(last >= 0) 
                    last += num;
                else 
                    last = num;
                
                res = max(res, last);
            }
            
        }
    }

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

贪心

【例题】耍杂技的牛

农民约翰的 N N N 头奶牛(编号为 1 ⋯ N 1\cdots N 1N)计划逃跑并加入马戏团,为此它们决定练习表演杂技。

奶牛们不是非常有创意,只提出了一个杂技表演:

叠罗汉,表演时,奶牛们站在彼此的身上,形成一个高高的垂直堆叠。

奶牛们正在试图找到自己在这个堆叠中应该所处的位置顺序。

N N N 头奶牛中的每一头都有着自己的重量 W i W_i Wi 以及自己的强壮程度 S i S_i Si

一头牛支撑不住的可能性取决于它头上所有牛的总重量(不包括它自己)减去它的身体强壮程度的值,现在称该数值为风险值,风险值越大,这只牛撑不住的可能性越高。

您的任务是确定奶牛的排序,使得所有奶牛的风险值中的最大值尽可能的小。

1 ≤ N ≤ 50000 , 1 ≤ W i ≤ 10 , 000 , 1 ≤ S i ≤ 1 , 000 , 000 , 000 1≤N≤50000,\\ 1≤Wi≤10,000 ,\\ 1≤Si≤1,000,000,000 1N50000,1Wi10,000,1Si1,000,000,000

分析:

这道题和国王游戏类似的,使用微扰来发现贪心策略。
最后就是按 S i × W i S_i\times W_i Si×Wi 从小到大排序就是最佳方案了。
证明就不细说了。

代码如下:

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 50010;

int n;
struct Cow {
    LL w, s;
    
    bool operator < (const Cow &_x) const {
        return w + s < _x.w + _x.s;
    }
    
} cow[N] ;

int main()
{
    scanf("%d", &n);
    
    for (int i = 0; i < n; i ++ )
        scanf("%lld%lld", &cow[i].w, &cow[i].s);
        
    sort(cow, cow + n);
    
    LL sum = 0, res = - 0x3f3f3f3f;
    
    for (int i = 0; i < n; i ++ ) {
        
        res = max(res, sum - cow[i].s);
        
        sum += cow[i].w;
        
    }
    
    printf("%lld", res);
    
    return 0;
}

【例题】任务

分析:

和防晒类似从一堆集合元素按照一定规则匹配另一堆集合元素。

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值