A-第k小数

这个题目有很多种做法(如果题目数据不大,卡的不死的话)
  1. 一个是直接排序然后拿出第K个就好了,直接用sort。(O(nlogn),最慢的)
  2. 一个是用STL的n_element函数,这个函数会将第k大元素放到第k个位置上。不会完全排序,所以快。
  3. 还有一个就是选择性快排,在选择上优化,不用快排的地方就不排了。


上面两个直接用STL函数的就不说了,就来讲一下快排做法
  1. 首先快排我们都知道,就是选取一个中间值mid,将比mid小的元素放在一边,比mid大的放在另一边
  2. 如果我们完全排序就和排序一样,没有区别了。那我上面说的选择上优化是什么意思呢?
  3. 其实简单来说:在排序后,我们已经能确定,mid的左右一边小一边大。我们按升序来说(左边小右边大)。
    那么现在有第k小数,k的值比mid的数组位置还大,说明k在数组的位置一定在mid后面
  4. 栗子:有100个数,我们已知当前mid在数组的第40个位置,我们现在求第44小的数,那mid一定在当前右边的区间里面。
  5. 那么我们已经知道k在哪边之后,另外一边就不用管了对吧(毕竟我们又不用排序,乱就乱呗)。
  6. 这就是我们的选择优化。


算法操作
  • 相比于正常的快排:
    void quickSort(int info[], int l, int r) {
        int i = l, j = r, mid = info[l + r >> 1];
        while (i <= j) {
            while (info[i] < mid) i++;
            while (mid < info[j]) j--;
            if (i <= j) {
                swap(info[i], info[j]);
                i++, j--;
            }
        }
        quickSort(info, l, j);
        quickSort(info, i, r);
    }
  • 我们只要在排序时加上特判就好了:
    if (l <= j && rk <= j - l + 1) quickselect(info, l, j, rk);
    //rk比中值小,快排左边,在左边依旧是第k小
    if (i <= r && rk >= i - l + 1) quickselect(info, i, r, rk - (i - l));
    //rk比中值大,快排右边,rk在右边的区间并不是第k小,而是第rk - (i - l)小


打代码
  1. STL的直接套函数就行了。
  2. 快排的就先输入数据。
  3. 然后快排呗,看上面。
  4. 下面代码嘞~


AC代码(sort排序)

#include <iostream>
#include <algorithm>
using namespace std;
//代码预处理区

const int MAX = 5e6 + 7;
int a[MAX];
//全局变量区
 
inline int read();
//函数预定义区
 
int main() {
    int T = read();
    while (T--) {
        int n = read(), k = read();
        for (int i = 1; i <= n; i++) a[i] = read();
        sort(a + 1, a + 1 + n);
        printf("%d\n", a[k]);
    }
    return 0;
}
inline int read(){
    int x = 0, f = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9'){
        if (ch == '-')
            f = -1;
        ch = getchar();
    }
    while(ch >= '0' && ch <= '9'){
        x = (x<<1) + (x<<3) + (ch^48);
        ch = getchar();
    }
    return x * f;
}
//函数区


AC代码(STL-nth_element函数)

#include <iostream>
#include <algorithm>
using namespace std;
//代码预处理区

const int MAX = 5e6 + 7;
int info[MAX];
//全局变量区

template<class T> inline void read(T& res);
template<class T> inline void write(T res);
//函数预定义区

int main() {
    int T; read(T);
    while (T--) {
        int n, k; read(n); read(k);
        for (int i = 1; i <= n; i++) read(info[i]);
        nth_element(info + 1, info + k, info + 1 + n);
        write(info[k]); putchar(10);
    }
    return 0;
}
template<class T> inline void read(T& res) {
    char c; int flag = 1;
    while ((c = getchar()) < '0' || c > '9')
        if (c == '-')
            flag = -1;
    res = c - '0';
    while ((c = getchar()) >= '0' && c <= '9')
        res = res * 10 + c - '0';
    res *= flag;
}
template<class T> inline void write(T res) {
    if(res < 0) {
        putchar('-');
        res = -res;
    }
    if(res > 9) write(res / 10);
    putchar(res % 10 + '0');
}
//函数区


AC代码(快速排序+选择优化)

#include <iostream>
using namespace std;
//代码预处理区

const int MAX = 5e6 + 7;
int info[MAX];
//全局变量区

template<class T> inline void read(T& res);//快读
template<class T> inline void write(T res);//快写
void quickselect(int info[], int l, int r, int rk);//选择快排
//函数预定义区

int main() {
    int T; read(T);
    while (T--) {
        int n, k; read(n); read(k);
        for (int i = 1; i <= n; i++) read(info[i]);
        quickselect(info, 1, n, k);
        write(info[k]); putchar(10);
    }
    return 0;
}
template<class T> inline void read(T& res) {
    char c; int flag = 1;
    while ((c = getchar()) < '0' || c > '9')
        if (c == '-')
            flag = -1;
    res = c - '0';
    while ((c = getchar()) >= '0' && c <= '9')
        res = res * 10 + c - '0';
    res *= flag;
}
template<class T> inline void write(T res) {
    if(res < 0) {
        putchar('-');
        res = -res;
    }
    if(res > 9) write(res / 10);
    putchar(res % 10 + '0');
}
void quickselect(int info[], int l, int r, int rk) {
    int i = l, j = r, mid = info[l + r >> 1];
    while (i <= j) {
        while (info[i] < mid) i++;
        while (mid < info[j]) j--;
        if (i <= j) {
            swap(info[i], info[j]);
            i++, j--;
        }
    }
    if (l <= j && rk <= j - l + 1) quickselect(info, l, j, rk);
    //rk比中值小,快排左边,在左边依旧是第k小
    if (i <= r && rk >= i - l + 1) quickselect(info, i, r, rk - (i - l));
    //rk比中值大,快排右边,rk在右边的区间并不是第k小,而是第rk - (i - l)小
}
//函数区


B-不平行的直线



AC代码



C-丢手绢

这道题我们可以用两种方法来写:三分法尺取法
  • 这道题两种方法我们都讲,用的都是单峰函数的性质。理所当然的,都要用到前缀和

首先我们还是先来看看题
  • 这道题数据有点问题,但是题目能看懂:就是告诉了你有个环,还有环的每条边长。求最远的两个点的距离


三分法分析:
  1. 看到这里,我们自然的就会想到,如果是一个点去判断最远的点怎么判断呢?
    很简单,遍历每个点去找从两端到达这个点的最小值中的最大值就好啦。但是这样写要O(n2),pass。
  2. 然后呢我们会发现,在选取每个起点的时候我们的距离一定是先增大,后减小,我们就发现了这是一个单峰的函数:这不就是三分吗!
  3. 三分法就是用来找峰值的。所以对于每个点做起点,我们用三分,对于所有的起点,我们遍历一遍,所以三分时间复杂度就是O(nlogn)。


尺取分析:
  1. 我们同样是发现,在选取每个起点的时候我们的距离一定是先增大,后减小。
    我们就知道,要判断两端到达这个点的最小值中的最大值
    遍历过去,只要后面的距离比前面的短,不就不用判断了吗。
  2. 栗子:长度5的环是1,1,1,1,1。判断这个的距离 < 下一个的距离,就继续,直到判断到这个的距离 > 下一个的距离,这个时候就没必要往下走了,因为是个单峰函数,就不会再更大了。
  3. 然后呢,在起点向后移动的时候,现在的答案理所当然的就变小了,要增大答案只能向后移动(也就是l后移了r也必须后移才能让答案更大),这就是为什么可以用尺取。


距离计算
  1. 所以接下来就是我们的距离应该怎么计算了,这里也就是我们上面说的一个很绕口的话:两端到达这个点的最小值中的最大值
  2. 首先我们要知道距离怎么计算:我一开始就说了要前缀和,这个毋庸置疑。但是另一点就是这是个环,一个点到另外一个点有两条路,我们要选较短的那条。
  3. 所以我们就可以得出:选较短的就用min求两条路径中的较小值,而两条路的较小值就用前缀和求出来:
    int dis(int u, int v) { return sum[u] - sum[v]; }
    int mn(int u, int v) { int len = dis(u, v); return min(len, sum[n + 1] - len); }


打代码(三分)
  1. 先初始化前缀和数组。
  2. 然后遍历每个起点,对每个起点的情况进行三分。
  3. 三分的时候选出当前起点的路径最大值,并用一个ans记录这些最大值里面的最大值。
  4. 看代码~

打代码(尺取)
  1. 先初始化前缀和数组。
  2. 然后开始尺取,枚举左右端点。
  3. 左端点随着外循环遍历而移动,右端点随着内循环计算最大值而移动。
  4. 再计算出并保存其中的最大值。
  5. 看代码~


AC代码(三分法)

#include <iostream>
#include <algorithm>
using namespace std;
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
//代码预处理区

const int MAX = 1e5 + 7;
int n, sum[MAX];//数据,正前缀和,反前缀和
//全局变量区

int dis(int u, int v) { return sum[u] - sum[v]; }
int mn(int u, int v) { int len = dis(u, v); return min(len, sum[n + 1] - len); }
//函数预定义区

int main() {
    IOS;
    cin >> n;
    for (int i = 2; i <= n + 1; i++) {
        cin >> sum[i];
        sum[i] += sum[i - 1];
    }
    int ans = 0;
    for (int i = 2; i <= n + 1; i++) {
        int l = 1, r = n, res = 0;
        while (r - l > 7) {
            int mid1 = l + (r - l) / 3;
            int mid2 = r - (r - l) / 3;
            if (mn(i, mid1) < mn(i, mid2)) {
                l = mid1; res = mn(i, mid2);
            }
            else {
                r = mid2; res = mn(i, mid1);
            }
            //二分求最近距离
        }
        for (int j = l; j <= r; j++) res = max(res, mn(i, j));
        //从这些剩下的里面求最远的距离
        ans = max(ans, res);
    }
    cout << ans << endl;
    return 0;
}
//函数区


AC代码(尺取法)

#include <iostream>
#include <algorithm>
using namespace std;
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
//代码预处理区

const int MAX = 1e5 + 7;
int n, sum[MAX];//数据,正前缀和,反前缀和
//全局变量区

int dis(int u, int v) { return sum[u] - sum[v]; }
int mn(int u, int v) { int len = dis(u, v); return min(len, sum[n + 1] - len); }
//函数预定义区

int main() {
    cin >> n;
    for (int i = 2; i <= n + 1; i++) {
        cin >> sum[i];
        sum[i] += sum[i - 1];
    }
    int ans = 0, r = 2;
    for (int l = 2; l <= n + 1; l++) {
        while (r < n && mn(l, r) < mn(l, r + 1)) r++;
        //循环到最大的位置(这是单峰函数,才能这样写的)
        ans = max(ans, mn(l, r));
        //从多组起点的情况选出最大值
    }
    cout << ans << endl;
    return 0;
}
//函数区


D-二分



AC代码



E-交换

这道题看题目要看清楚,一开始我还以为是求逆序对,但是现在才发现交换不一定相邻。

首先我们来看题目
  • 题目是说,我们要把这个数组换成有序,最少要交换多少次数据


算法思想
  1. 既然我们要知道从当前的乱序到最终的有序要进行多少步,我们可以把初态和终态保存下来做对比
  2. 我们这里当然可以用两个数组来表示,但是我们还有一个简单的方法就是用结构体或者pair(就是包装过的只有两项的结构体)。
  3. 我这里用了pair,pair在sort时会默认按first项排升序,然后我们排序后的first项就是有序的(现在的位置),second就是以前的(乱序时的位置
  4. 那么我们就可以发现:我们的first是从second来的!与此同时,排序后second的位置也是从某一个位置来的,一直往复下去,一定会有一个节点是从first中来的(形成环)。
  5. 栗子:就拿3,2,1来说,最后1和3是换了位置的对吧,也就是说1和3形成了一个环。这个环移动一次就可以让数据归位。
  6. 从上面我们也能观察到,如果是更大的环,有n个节点的环,移动n - 1个数就能使序列有序。反过来看,就是有一个环,可以让交换次数比节点个数少1
  7. 所以不难得出,我们最多数组也就是换个n次。然后有一个环就次数-1,答案不就是(n - 环数)吗(注意:<stron>呗)。 </stron>


说到判环和搜索,我们自然的就会想到dfs
  1. 所以我们为了判环,加一个标记数组visited。用来表示这些节点有没有找过
  2. 判环基本操作:我们以每一个节点为原点进行找环,没有被查找过就继续找,然后把这个节点所在的环遍历掉,最后计个数就好了。

dfs操作
  • 其实很简单,用个变量记录下起点,然后不停的找起点所对应的来源点。一直找到找回自己为止(这样就遍历完了一个环)。


打代码呗:
  1. 首先我们输入变量,存进pair嘛。
  2. 然后给结构体数组排个序。
  3. 最后判环统计就好了。
  4. 看我代码呗~


AC代码

#include <iostream>
#include <algorithm>
#include <utility>
using namespace std;
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
//代码预处理区

const int MAX = 1e5 + 7;
pair<int, int> info[MAX];
bool visited[MAX];
//全局变量区

int main() {
    IOS;
    int n; cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> info[i].first;
        info[i].second = i;
    }
    sort(info + 1, info + 1 + n);
    int cnt = 0;
    for (int i = 1; i <= n; i++) {
        if (visited[i]) continue;
        //在以前形成过环
        auto it = info[i];
        while (!visited[it.second]) {
            visited[it.second] = true;
            it = info[it.second];
        }
        //标记环
        cnt++;//环数+1
    }
    cout << n - cnt << endl;
    return 0;
}
//函数区