A-第k小数
这个题目有很多种做法(如果题目数据不大,卡的不死的话)
- 一个是直接排序然后拿出第K个就好了,直接用sort。(O(nlogn),最慢的)
- 一个是用STL的n_element函数,这个函数会将第k大元素放到第k个位置上。不会完全排序,所以快。
- 还有一个就是选择性快排,在选择上优化,不用快排的地方就不排了。
上面两个直接用STL函数的就不说了,就来讲一下快排做法:
- 首先快排我们都知道,就是选取一个中间值mid,将比mid小的元素放在一边,比mid大的放在另一边。
- 如果我们完全排序就和排序一样,没有区别了。那我上面说的选择上优化是什么意思呢?
- 其实简单来说:在排序后,我们已经能确定,mid的左右一边小一边大。我们按升序来说(左边小右边大)。
那么现在有第k小数,k的值比mid的数组位置还大,说明k在数组的位置一定在mid后面。 - 栗子:有100个数,我们已知当前mid在数组的第40个位置,我们现在求第44小的数,那mid一定在当前右边的区间里面。
- 那么我们已经知道k在哪边之后,另外一边就不用管了对吧(毕竟我们又不用排序,乱就乱呗)。
- 这就是我们的选择优化。
算法操作:
- 相比于正常的快排:
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)小
打代码:
- STL的直接套函数就行了。
- 快排的就先输入数据。
- 然后快排呗,看上面。
- 下面代码嘞~
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-丢手绢
这道题我们可以用两种方法来写:三分法,尺取法。
- 这道题两种方法我们都讲,用的都是单峰函数的性质。理所当然的,都要用到前缀和。
首先我们还是先来看看题:
- 这道题数据有点问题,但是题目能看懂:就是告诉了你有个环,还有环的每条边长。求最远的两个点的距离。
三分法分析:
- 看到这里,我们自然的就会想到,如果是一个点去判断最远的点怎么判断呢?
很简单,遍历每个点去找从两端到达这个点的最小值中的最大值就好啦。但是这样写要O(n2),pass。 - 然后呢我们会发现,在选取每个起点的时候我们的距离一定是先增大,后减小,我们就发现了这是一个单峰的函数:这不就是三分吗!
- 三分法就是用来找峰值的。所以对于每个点做起点,我们用三分,对于所有的起点,我们遍历一遍,所以三分时间复杂度就是O(nlogn)。
尺取法分析:
- 我们同样是发现,在选取每个起点的时候我们的距离一定是先增大,后减小。
我们就知道,要判断两端到达这个点的最小值中的最大值。
遍历过去,只要后面的距离比前面的短,不就不用判断了吗。 - 栗子:长度5的环是1,1,1,1,1。判断这个的距离 < 下一个的距离,就继续,直到判断到这个的距离 > 下一个的距离,这个时候就没必要往下走了,因为是个单峰函数,就不会再更大了。
- 然后呢,在起点向后移动的时候,现在的答案理所当然的就变小了,要增大答案只能向后移动(也就是l后移了r也必须后移才能让答案更大),这就是为什么可以用尺取。
距离计算:
- 所以接下来就是我们的距离应该怎么计算了,这里也就是我们上面说的一个很绕口的话:两端到达这个点的最小值中的最大值。
- 首先我们要知道距离怎么计算:我一开始就说了要前缀和,这个毋庸置疑。但是另一点就是这是个环,一个点到另外一个点有两条路,我们要选较短的那条。
- 所以我们就可以得出:选较短的就用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); }
打代码(三分):
- 先初始化前缀和数组。
- 然后遍历每个起点,对每个起点的情况进行三分。
- 三分的时候选出当前起点的路径最大值,并用一个ans记录这些最大值里面的最大值。
- 看代码~
打代码(尺取):
- 先初始化前缀和数组。
- 然后开始尺取,枚举左右端点。
- 左端点随着外循环遍历而移动,右端点随着内循环计算最大值而移动。
- 再计算出并保存其中的最大值。
- 看代码~
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-交换
这道题看题目要看清楚,一开始我还以为是求逆序对,但是现在才发现交换不一定相邻。
首先我们来看题目:
- 题目是说,我们要把这个数组换成有序,最少要交换多少次数据。
算法思想:
- 既然我们要知道从当前的乱序到最终的有序要进行多少步,我们可以把初态和终态保存下来做对比。
- 我们这里当然可以用两个数组来表示,但是我们还有一个简单的方法就是用结构体或者pair(就是包装过的只有两项的结构体)。
- 我这里用了pair,pair在sort时会默认按first项排升序,然后我们排序后的first项就是有序的(现在的位置),second就是以前的(乱序时的位置)。
- 那么我们就可以发现:我们的first是从second来的!与此同时,排序后second的位置也是从某一个位置来的,一直往复下去,一定会有一个节点是从first中来的(形成环)。
- 栗子:就拿3,2,1来说,最后1和3是换了位置的对吧,也就是说1和3形成了一个环。这个环移动一次就可以让数据归位。
- 从上面我们也能观察到,如果是更大的环,有n个节点的环,移动n - 1个数就能使序列有序。反过来看,就是有一个环,可以让交换次数比节点个数少1。
- 所以不难得出,我们最多数组也就是换个n次。然后有一个环就次数-1,答案不就是(n - 环数)吗(注意:<stron>呗)。 </stron>
说到判环和搜索,我们自然的就会想到dfs:
- 所以我们为了判环,加一个标记数组visited。用来表示这些节点有没有找过。
- 判环基本操作:我们以每一个节点为原点进行找环,没有被查找过就继续找,然后把这个节点所在的环遍历掉,最后计个数就好了。
dfs操作:
- 其实很简单,用个变量记录下起点,然后不停的找起点所对应的来源点。一直找到找回自己为止(这样就遍历完了一个环)。
打代码呗:
- 首先我们输入变量,存进pair嘛。
- 然后给结构体数组排个序。
- 最后判环统计就好了。
- 看我代码呗~
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; } //函数区