算法笔记第四章
4.2 散列
散列分为整数散列和字符串散列,当出现冲突时有三种方法解决冲突:
1.线性探查法: 检查H(key)+1是否被占,若没有使用该位置如果被占继续检查下一个。如果检查过程中超过了表长就回到表首位继续循环。缺点是容易出现扎堆现象。
2.平方探查法: 为了防止出现扎堆现象,按照H(key)+12,H(key)-12,H(key)+2,H(key)-22的顺序检查。如果检查过程中H(key)+k2超过表长就把H(key)k对表长Tsize取模,如果检查过程中H(key)-k2 < 0则进行((H(key)-k2)%Tsize +Tsizie )%Tsize处理。
3.链地址法: 将H(key)相同的key连接成单链表。
字符串哈希初步
字符串哈希函数
int hashFunc(char s[], int len)
{
int id = 0;
for(int i = 0; i < len; i++)
{
id = id * 26 + (s[i] - 'A');
}
return id;
}
转换的最大数字为26len - 1
如果字符串中出现了大小写字母:
int hashFunc(char s[], int len)
{
int id = 0;
for(int i = 0; i < len; i++)
{
if(s[i] >= 'A' && s[i] <= 'Z')
id = id * 52 + (s[i] - 'A');
else if(s[i] >= 'a' && s[i] <= 'z')
id = id * 52 + (s[i] - 'z') + 26;
}
return id;
}
如果字符串不仅有字母还有数字:
1.按照刚才的思路,将进制增大为62
2.如果只是末尾的k个数为数字,则可以循环到len - k,再加上数字哈希。
int hashFunc(char s[], int len, int k)
{
int id = 0, i;
for(i = 0; i < len - k; i++)
{
if(s[i] >= 'A' && s[i] <= 'Z')
id = id * 52 + (s[i] - 'A');
else if(s[i] >= 'a' && s[i] <= 'z')
id = id * 52 + (s[i] - 'a') + 26;
}
while(i < len)
{
id = id * 10 + (s[i] - '0');
}
return id;
}
pat题目
A1084
主要考虑如何解决
-
在不区分大小写的情况下,第一个字符串的哪些字符在第二个字符串没有出现。
解决方法:暴力搜索,对第一个字符串进行遍历,对每一个字符进入内循环对第二个字符串进行遍历,查找是否有相同元素。并且每次都将小写转换为大写。 -
如何保证只输出一次
解决方法:建立hash表,每次输出后的字母进行标记。
#include<cstdio>
#include<cstring>
bool hashtable[128] = {false};
int main()
{
char str1[100], str2[100];
scanf("%s %s", str1, str2);
int len1 = strlen(str1);
int len2 = strlen(str2);
for(int i = 0; i < len1; i++)
{
int j;
char c1, c2;
for(j = 0; j < len2; j++)
{
c1 = str1[i];
c2 = str2[j];
if(c1 <= 'z' && c1 >= 'a') c1 -= 32;
if(c2 <= 'z' && c2 >= 'a') c2 -= 32;
if(c1 == c2) break;
}
if(j == len2 && hashtable[c1] == false)
{
printf("%c", c1);
hashtable[c1] = true;
}
}
return 0;
}
B1033
使用hash将坏键盘标记,同时将大写字母全部转换为小写字母。对第二个字符串遍历时,如果字母为大写则检查它的小写字母和字母‘+’的hash状态。如果为小写字母则直接检查这个字母的hash状态即可。
#include<cstdio>
#include<cstring>
const int N = 100010;
bool hashTable[128];
char s1[N], s2[N];
int main()
{
memset(hashTable, true, sizeof(hashTable));
fgets(s1, N, stdin);
int len1 = 0;
while(s1[len1] != '\n') len1++;
s1[len1] = '\0';
fgets(s2, N, stdin);
int len2 = 0;
while(s2[len2] != '\n') len2++;
s1[len2] = '\0';
for(int i = 0; i < len1; i++)
{
char c1 = s1[i];
if(c1 >= 'A' && c1 <= 'Z') c1 = c1 - 'A' + 'a';
hashTable[c1] = false;
}
for(int i = 0; i < len2; i++)
{
char c2 = s2[i];
if(c2 >= 'A' && c2 <= 'Z')
{
if(hashTable[c2 + 32] && hashTable['+'])
printf("%c", s2[i]);
}
else
{
if(hashTable[c2])
printf("%c", s2[i]);
}
}
printf("\n");
return 0;
}
A1092
本题可以直接用hash[256]来简化代码步骤。因为珠子可能会重复所以不能用bool类型而要用int类型。先对第一个字符串进行处理,遇到珠子的时候就++,然后再对第二个字符串,如果当前字符对应的int数组为0 说明没有需要的珠子,可以设置一个flag,如果当前数组不为0则int数组-1即可。
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 1010;
int hashtable[80];
int main()
{
int miss = 0;
memset(hashtable, 0, sizeof(hashtable));
char str1[N], str2[N];
scanf("%s %s", str1, str2);
int len1 = strlen(str1);
int len2 = strlen(str2);
for(int i = 0;i < len1; i++)
{
int c1 = str1[i];
if(str1[i] >= 'a' && str1[i] <= 'z') c1 = c1 - 'a' + 10;
if(str1[i] >= 'A' && str1[i] <= 'Z') c1 = c1 - 'A' + 36;
if(str1[i] >= '0' && str1[i] <= '9') c1 = c1 - '0';
hashtable[c1]++;
}
for(int i = 0; i < len2; i++)
{
int c2 = str2[i];
if(str2[i] >= 'a' && str2[i] <= 'z') c2 = str2[i] - 'a' + 10;
if(str2[i] >= 'A' && str2[i] <= 'Z') c2 = str2[i] - 'A' + 36;
if(str2[i] >= '0' && str2[i] <= '9') c2 = str2[i] - '0';
hashtable[c2]--;
if(hashtable[c2] < 0)
{
miss++;
}
}
if(miss > 0)
{
printf("No %d", miss);
}
else
printf("Yes %d", len1 - len2);
return 0;
}
B1042
本题只需要考虑字母的处理,所以建立一个30大小的哈希数组,遍历时如果字符是字母就让哈希数组加1,最后对哈希数组遍历找到最大值即可。
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
const int N = 1010;
int hashtable[30] = {0};
int main()
{
char str[N];
fgets(str, sizeof(str), stdin);
int len = 0;
while(str[len] != '\n') len++;
str[len] = '\0';
int ans = 0, id = -1;;
for(int i = 0; i < len; i++)
{
if(str[i] >= 'a' && str[i] <= 'z')
{
hashtable[str[i] - 'a']++;
}
else if(str[i] >= 'A' && str[i] <= 'Z')
{
hashtable[str[i] - 'A']++;
}
}
int k = 0;
for(int i = 0; i < 30; i++)
{
if(hashtable[i] > hashtable[k])
k = i;
}
printf("%c %d\n", k + 'a', hashtable[k]);
return 0;
}
A1041
本题相当于设置hash数组,然后按照输入的顺序来找到第一个hash数组值为1的下标。所以需要在建立一个rec[N]数组用来存储每次输入的数据,然后根据rec来遍历即可。
#include<cstdio>
#include<cstring>
const int N = 100010;
int hashtable[N], rec[N] = {0};
int main()
{
memset(hashtable, 0, sizeof(hashtable));
int n;
scanf("%d", &n);
int k, p = -1;
for(int i = 0; i < n; i++)
{
scanf("%d", &rec[i]);
hashtable[rec[i]]++;
}
int i;
for(i = 0; i < n; i++)
{
if(hashtable[rec[i]] == 1)
{
printf("%d\n", rec[i]);
break;
}
}
if(i == n)
printf("None\n");
return 0;
}
A1050
利用散列思想,定义hash数组,先将hash[s2[i]]设置false,再对s1进行遍历,如果hash[s1[i]]为true则进行输出。
#include<cstdio>
#include<cstring>
const int N = 10010;
char str1[N], str2[N];
bool hashTable[256];
int main()
{
memset(hashTable, true, sizeof(hashTable));
fgets(str1, N, stdin);
int len1 = 0;
while(str1[len1] != '\n') len1++;
str1[len1] = '\0';
fgets(str2, N, stdin);
int len2 = 0;
while(str2[len2] != '\n') len2++;
str2[len2] = '\0';
for(int i = 0; i < len2; i++)
{
hashTable[str2[i]] = false;
}
for(int i = 0; i < len1; i++)
{
if(hashTable[str1[i]])
printf("%c", str1[i]);
}
return 0;
}
B1005
同样用hash来处理,首先对每个输入的数字进行(3n+1)处理,并且将每次处理的值对应的hash更新。因为最后一个不能有空行,所以需要两次遍历,第一次遍历找到有多少个关键字,第二次输出。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 10010;
bool Hash[N];
int rec[110];
bool cmp(int a, int b)
{
return a > b;
}
int main()
{
memset(Hash, false , sizeof(Hash));
int n, m;
scanf("%d", &n);
for(int i = 0; i < n; i++)
{
scanf("%d", &rec[i]);
m = rec[i];
while(m != 1)
{
if(m % 2 == 1)
m = (m * 3 + 1) / 2;
else
m = m / 2;
Hash[m] = true;
}
}
int num = 0;
for(int i = 0; i < n; i++)
{
if(Hash[rec[i]] == false)
num++;
}
sort(rec, rec + n, cmp);
for(int i = 0; i < n; i++)
{
if(Hash[rec[i]] == false)
{
printf("%d", rec[i]);
num--;
if(num != 0) printf(" ");
}
}
return 0;
}
A1048(需要学习代码写法)
本题有多种方法可以做,hash,二分,双指针。在这里先用hash来做。将每次读入的数字对应的hash数组元素+1,然后对hash数组进行遍历,当找到i和m-i不为0的时候进行判断i和m-i是否相等,如果相等还要保证hash[i]>=2即可。
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 100010;
int hashTable[510] = {0}, rec[N];
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for(int i = 0; i < n; i++)
{
scanf("%d", &rec[i]);
hashTable[rec[i]]++;
}
int i;
for(i = 1; i <= 500; i++)
{
if(hashTable[i] != 0 && hashTable[m - i]!= 0)
{
if(hashTable[i] && hashTable[m - i])
{
if(i == m - i && hashTable[i] <= 1)
continue;
printf("%d %d\n", i, m - i);
break;
}
}
}
if(i > 500) printf("No Solution\n");
return 0;
}
和gets同样的写法
int len = 0;
while(true)
{
s[len++] = getchar();
if(s[len - 1] == '\n') break;
}
4.3 递归
分治法就是将原始问题分成规模较小而结构与原始问题相同或类似的子问题,然后分别解决这些子问题,最后合并子问题的解即可得到原问题的解。
分治法既可以使用递归的手段解决也可以使用非递归的手段实现
全排列问题
把1~n按个整数按字典序排列,输出这些排列
解题思路: 从递归角度来看,问题就可以被分解成:”输出1开头的全排列“,”输出以2开头的全排列“,…”输出以n开头的全排列。所以可以先设定一个hash数组,其中hash[x]当x已经在数组时为false。
递归细节: 现在开始按第1位到第n位填入数组,假设已经填好了p[1]~p[index-1]。准备填入p[index]时,则有p[index] = x,hashtable[x] = true。表示x已经被填入,同时递归去处理第index+1位。当递归完成时把hashtable[x]还原为true,以便让p[index]填下一个数字。
边界条件: 当index == n时说明数组已经全部填完了,既可输出数组并返回。
#include<cstdio>
#include<cstring>
int a[3];
bool hashtable[3];
void allsort(int index, int n)
{
if(index == n)
{
for(int i = 0; i < n; i++)
printf("%d", a[i]);
printf("\n");
return;
}
for(int i = 0; i < n; i++)
{
if(hashtable[i] == true)
{
hashtable[i] = false;
a[index] = i+1;
allsort(index+1, n);
hashtable[i] = true;
}
}
}
int main()
{
memset(hashtable, true, sizeof(hashtable));
allsort(0,3);
return 0;
}
n皇后问题
在n*n的棋盘上放n个皇后,使得这n个皇后两两均不在同一行,同一列,同一个对角线上。求合法的方案数。
解题思路: 由于每行和每列只能放一个皇后,如果把n列的皇后所在的行号依次写出,就是一个1~n的排列,就和上面的全排列是一样的了。所以只需要在边界的时候统计合法方案即可。
递归细节: 本题递归其实和全排列一样,因为求n个皇后的排列方法其实就是求1~n个数的全排列问题。
边界条件: 在已经完成的排列进行一下特判,并用全局变量记录即可。
#include<cstdio>
#include<cstring>
#include<cmath>
int a[10];
bool hashtable[10];
int cnt;
void allsort(int index, int n)
{
if(index == n + 1)
{
bool flag = true;
for(int i = 1; i <= n; i++)
{
for(int j = i + 1; j <= n; j++)
{
if(abs(j - i) == abs(a[j] - a[i]))
{
flag = false;
}
}
}
if(flag) cnt++;
return;
}
for(int i = 1; i <= n; i++)
{
if(hashtable[i] == true)
{
hashtable[i] = false;
a[index] = i;
allsort(index+1, n);
hashtable[i] = true;
}
}
}
int main()
{
memset(hashtable, true, sizeof(hashtable));
cnt = 0;
allsort(1,8);
printf("%d", cnt);
return 0;
}
这种枚举所有方法然后判断每一种情况是否合法是非常朴素的,当已经放置了一部分皇后时,可能剩余的皇后怎么放都不合法。因此就没必要往下递归了,直接返回上层即可。
一般来说在到达递归边界前的某层由于一些事实导致已经不需要下一个递归就可以直接返回上一层。这种做法一般称为 回溯法。
void allsort(int index, int n) //在第index列放一个皇后
{
if(index == n + 1) // 如果能到达边界条件,则一定合法
{
if(index == n + 1)
{
cnt++;
return;
}
}
for(int i = 1; i <= n; i++) // 第x行
{
if(hashtable[i] == true) //第x行还没放
{
bool flag = true; //flag为true表示不会和之前的皇后起冲突
for(int pre = 1; pre < index; pre++) //遍历之前的皇后
{
if(abs(pre - index) == abs(a[pre] - i))
{
flag = false;
break;
}
}
if(flag)
{
hashtable[i] = false;
a[index] = i;
allsort(index+1, n);
hashtable[i] = true;
}
}
}
}
4.4 贪心
简单贪心
贪心是求解一类最优化问题的方法。总是考虑在当下状态下的局部最优或较优策略,来使全局达到最优或较优。严谨使用贪心法来求解最优化问题需要对策略进行证明。
PAT题目
B1020
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 1010;
struct mookcake
{
double store;
double sell;
double price;
}cake[N];
bool cmp(mookcake a, mookcake b)
{
return a.price > b.price;
}
int main()
{
int n, d;
scanf("%d %d", &n, &d);
for(int i = 0; i < n; i++)
{
scanf("%lf", &cake[i].store);
}
for(int i = 0; i < n; i++)
{
scanf("%lf", &cake[i].sell);
cake[i].price = cake[i].sell / cake[i].store;
}
sort(cake, cake + n, cmp);
double ans = 0;
for(int i = 0; i < n; i++)
{
if(cake[i].store <= d)
{
d -= cake[i].store;
ans += cake[i].sell;
}
else
{
ans += cake[i].price * d;
break;
}
}
printf("%.02f\n", ans);
return 0;
}
B1023
#include<cstdio>
int hash[10];
int main()
{
for(int i = 0; i < 10; i++)
{
scanf("%d", &hash[i]);
}
for(int i = 1; i < 10; i++)
{
if(hash[i] != 0)
{
printf("%d", i);
hash[i]--;
break;
}
}
for(int i = 0; i < 10; i++)
{
if(hash[i] != 0)
{
for(int j = 0; j < hash[i]; j++)
printf("%d", i);
}
}
return 0;
}
A1033(难)
本题在于数据的处理每次贪心的更新需要注意。策略就是先将每个station以dis降序排列,如果第一个station的dis != 0则说明无法出发,否则可以出发,设置每段最大能走的路,当前的油量,当前所在的加油站序号。每次先在max范围内进行遍历找到油价最低的加油站,将油价最低的加油站编号和油价记录下来,并计算从当前到加油站所需要多少油。下面进行贪心策略,如果目标加油站的油价比当前加油站价格低则只需要加到刚好够到目标加油站的油量,如果比当前加油站高,则油全部加满。 如果没找到则直接结束算法。最后根据结束循环时的 加油站编号是否为最后一个加油站来确定是否到达终点,并输出。
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 510;
const int INF = (1 << 31) - 1;
struct station
{
double dis, price;
}sta[N];
bool cmp(station a, station b)
{
return a.dis < b.dis;
}
int main()
{
int n;
double Cmax, D, Davg;
scanf("%lf%lf%lf%d", &Cmax, &D, &Davg, &n);
for(int i = 0; i < n; i++)
{
scanf("%lf%lf", &sta[i].price, &sta[i].dis);
}
sta[n].price = 0;
sta[n].dis = D;
sort(sta, sta + n, cmp);
if(sta[0].dis != 0)
{
printf("The maximum travel distance = 0.00\n");
}
else
{
int now = 0;
double ans = 0, nowTank = 0, MAX = Davg * Cmax;
while(now < n)
{
int k = -1;
double priceMin = INF;
for(int i = now + 1; i <= n && sta[i].dis - sta[now].dis <= MAX; i++)
{
if(sta[i].price < priceMin)
{
priceMin = sta[i].price;
k = i;
}
if(priceMin < sta[now].price)
break;
}
if(k == -1) break;
double need = (sta[k].dis - sta[now].dis) / Davg;
if(priceMin < sta[now].price)
{
if(nowTank < need)
{
ans += (need - nowTank) * sta[now].price;
nowTank = 0;
}
else
nowTank -= need;
}
else
{
ans += (Cmax - nowTank) * sta[now].price;
nowTank = Cmax - need;
}
now = k;
}
if(now == n)
printf("%.2f", ans);
else
printf("The maximum travel distance = %.2f\n", sta[now].dis + MAX);
}
return 0;
}
A1037
本题使用贪心策略,因为要使得总体最大,所以只需要让最大的正数×最大的正数,最小的负数×最小的负数即可。
证明:
假设a>b>0, c>d>0, 下证ac+bd > ad + bc
∵a>b
∴a - b > 0
∴(a - b)c > (a - b)d
∴ac + bd > ad + bc
证毕
注意负数和负数相乘时的条件应该是两个都小于0,而不是他们的乘积大于0,否则无法分别是两个正数相乘还是两个负数相乘。如果数据不能全过,则可能是数据越界,改成long long更保险
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 100010;
int coupon[N], product[N];
int main()
{
int n, m;
scanf("%d", &n);
for(int i = 0; i < n; i++)
scanf("%d", &coupon[i]);
scanf("%d", &m);
for(int i = 0; i < m; i++)
scanf("%d", &product[i]);
sort(coupon, coupon + n);
sort(product, product + m);
int i = 0, j;
long long ans = 0;
while(i < n && i < m && product[i] < 0 && coupon[i] < 0)
{
ans += coupon[i] * product[i];
i++;
}
i = n - 1;
j = m - 1;
while(i >= 0 && j >= 0 && coupon[i] > 0 && product[j] > 0)
{
ans += coupon[i] * product[j];
i--, j--;
}
printf("%d", ans);
return 0;
}
A1067
本题的思路是若使得总步数最少则无效步数越少越好,对于数字0不在0号位的情况时:先找0所在的位置,设为k号位,再找到数字k所在的位置并进行互换则可以保证k一定归位了。这种情况为有效步数,若0在第0位,则我们需要找到一个没有归位的并且两个进行交换,这种情况为无效步数。并且可以发现如果归位后的数字将不会再改变,所以我们可以设置一个k来寻找目前序列位不在本位的最小数。这样就能保证每次寻找整体上都是线性的。因为我们的关键是要找到每个数字的下标,所以我们在处理输入的数据时,应该将每个数字的大小当成下标,而将这个数字所在的序列位当成元素值,这样我们寻找值为k数字的下标就可以使用pos[k],对情况1就可以使用代码swap(pos[0], pos[pos[0]),简化了代码寻找位置的过程
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 100010;
int pos[N];
int main()
{
int n, num;
scanf("%d", &n);
int left = n - 1;
for(int i = 0; i < n; i++)
{
scanf("%d", &num);
pos[num] = i;
if(num > 0 && num == i)
left--;
}
int k = 1, ans = 0;
while(left > 0)
{
if(pos[0] == 0)
{
while(k < n)
{
if(pos[k] != k)
{
swap(pos[0], pos[k]);
ans++;
break;
}
k++;
}
}
while(pos[0] != 0)
{
swap(pos[0], pos[pos[0]]);
ans++;
left--;
}
}
printf("%d\n", ans);
return 0;
}
A1038
本题还是需要使用贪心算法,假设s1+s2 < s2 + s1(加号表示拼接)那么s1就放在s2前面。严谨证明略。具体实现时可以用cmp函数实现,直接用sort函数即可。同时如果用二维数组会比较麻烦,用string会大大简化代码,需要注意输出前导0
#include<cstdio>
#include<algorithm>
#include<string>
#include<iostream>
using namespace std;
const int N = 10010;
string str[N];
bool cmp(string a, string b)
{
return a + b < b + a;
}
int main()
{
int n;
scanf("%d", &n);
for(int i = 0; i < n; i++)
cin >> str[i];
sort(str, str + n, cmp);
string ans;
for(int i = 0; i < n; i++)
ans += str[i];
while(ans.size() != 0 && ans[0] == '0')
{
ans.erase(ans.begin());
}
if(ans.size() == 0) cout << 0;
else cout << ans;
return 0;
}
区间贪心
给出N个开区间(x,y),从中选择尽可能多的开区间使得这些开区间两两没有交集。
1.当I1被I2包含时,显然选择I1更好。
2.接下来把所有区间的左端点从大到小排列,去掉包含区间的情况,则左端点大的右端点一定更大。所以当把左端点大的区间的多余部分去掉就相当于1的情况。所以总是优先选择左端点较大的。
综上,先选左端点大的,如果左端点相同就先选区间小的。所以可以根据此写出cmp函数,在比较时,只要新的区间的右端点小于当前维护区间的左端点,既可进行更新。
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 1010;
struct Inteval
{
int x, y;
}I[N];
bool cmp(Inteval a, Inteval b)
{
if(a.x != b.x) return a.x > b.x;
else return a.y < b.y;
}
int main()
{
int n;
scanf("%d", &n);
for(int i = 0; i < n; i++)
scanf("%d%d", &I[i].x, &I[i].y);
sort(I, I + n, cmp);
int ans = 1, lastX = I[0].x;
for(int i = 1; i < n; i++)
{
if(I[i].y <= x)
{
ans++;
lastX = I[i].x;
}
}
printf("%d\n", ans);
}
类似问题: 给出N个闭区间[x,y],求最少确定多少个点才能使得每个区间都至少存在一个点。
解题策略与区间不相交问题是一致的
1.如果I1被I2包含,则选择I1一定可以保证在I2内。
2.将区间按左端点从大到小排列,对于左端点大的来说 取左端点 就可以使它覆盖更多的区间。所以只需要将I[i].y <= lastX改成I[i].y < lastX即可。
4.5 二分
二分查找
问题:给定一个严格增序A中找出给的数X
-
用于查找数组里是否有某个元素,因为当left == right时,依然需要判断当前数据是否满足条件。所以循环条件时小于等于。
-
代码默认为递增,如果时递减则把A[mid] >x换成A[mid] < x即可。
-
如果查询元素靠后,(left+right)/2可能会越界,换成mid = left +(right - left) /2
int binarySearch(int left, int right, int x)
{
int mid;
while(left <= right)
{
mid = (left + right) / 2;
if(A[mid] == x) return mid;
else if(A[mid] > x) right = mid - 1;
else left = mid + 1;
}
return -1;
}
如果递增序列A中有重复元素,那么如何给定预查询元素x,求出序列中第一个大于等于x的位置L,以及第一个大于x的元素的位置R,这样元素x在序列中存在区间就是左闭右开区间[L, R)。如果序列中没有元素x,那么L和R可以理解成假设序列中存在x,x应当存在的位置。
求序列中第一个大于等于x的代码:
int low_bound(int A[], int left, int right, int x)
{
int mid;
while(left < right)
{
mid = left + right >> 1;
if(A[mid] >= x)
right = mid;
else
left = mid + 1;
}
return left;
}
注意:
- 循环条件为left < right,而非之前的left<= right。是因为上一个当元素不存在时需要返回一个-1,这样当left<right时就不再是闭区间,可以作为元素不存在的依据。而如果返回第一个大于等于x的位置不需要判断x存不存在,即使不存在返回的也是假设存在,它应该存在的位置。
- 返回值既可以是left也可以是right,因为结束时left == right
- 二分的上界应该是n,而不是n-1,因为欲查询元素可能比所有x都大,这时返回的是它应该存在的位置,是n。所以[left, right] = [0, n]。
求序列第一个大于元素x 的位置:
int upper_bound(int A[], int left, int right, int x)
{
int mid;
while(left < right)
{
mid = left + right >> 1;
if(A[mid] > x)
right = mid;
else
left = mid + 1;
}
return left;
}
只需要将判断条件的大于等于改成大于即可。
其实上述两个代码都是解决这样一个问题:寻找序列中第一个满足某条件的元素的位置。 所谓的某条件一定是先不满足后满足。
int solve(int left, int right, int x)
{
int mid;
while(left < right)
{
mid = left + right >> 1;
if( 条件成立 )
right = mid;
else
left = mid + 1;
}
return left;
}
如果想要寻找最后一个满足条件c的位置,条件c是先满足后不满足,那么我们可以求得第一个满足“条件!c”的位置。再将该位置减1即可。或者使用下述代码模板:
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
如果只是寻找 序列中是否存在满足某条件的元素 那么用最开始二分查找最合适。
A1085
本题用二分搜索,对每一个i找出满足条件的j,找到j-i的最大值即可。但是需要注意二分的边界,因为我们要找的是先满足后不满足的条件所以注意二分的更新方式。同时要注意边界,因为我们要找的最大值 最多只能是到n-1而不是n 所以上界为n-1。因为所有元素均可能达到109所以函数接口因为long long
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 100010;
int a[N];
int binary_search(int left, int right, long long x)
{
int mid;
while(left < right)
{
mid = left + right + 1 >> 1;
if(a[mid] <= x)
left = mid;
else
right = mid - 1;
}
return left;
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for(int i = 0; i < n; i++)
scanf("%d", &a[i]);
sort(a, a + n);
int ans = 0;
for(int i = 0; i < n; i++)
{
int pos = binary_search(0, n - 1, (long long)a[i] * m);
ans = max(ans, pos - i + 1);
}
printf("%d", ans);
return 0;
}
A1010(难)
对一个确定的数字串来说,它的进制越大,转换成十进制结果越大,所以可以利用二分.难点是如何找到left和right 的值,以及如何进行比较处理.
- 字符串转换成十进制的函数,需要定义一个int数组用来建立字符和数字直接的对应.同时在转换函数中需要一个上界,如果超过了则直接返回-1,否则返回转换后的数字.
- 比较函数,用于将给定radix转换后的数字和已知数据进行比较,结果可以分成-1,0,1.
- 二分函数,因为本题是查找left和right之间存不存在某个radix,所以用最开始的二分查找模板即可.
- 为了简化代码,首先将已知的字符串存放在n1,未知的存放n2,同时 二分查找的下届是n2进制的最大值加1,上界是left和n1值的最大值加1
- 尽量使用long long 来处理数据,以防数据越界.
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long LL;
LL Map[256];
LL inf = (1LL << 63) - 1;
void init()
{
for(char c = '0'; c <= '9'; c++)
Map[c] = c - '0';
for(char c = 'a'; c <= 'z'; c++)
Map[c] = c - 'a' + 10;
}
LL converNum10(char a[], LL radix, LL t)
{
LL ans = 0;
int len = strlen(a);
for(int i = 0; i < len; i++)
{
ans = ans * radix + Map[a[i]];
if(ans < 0 || ans > t)
return -1;
}
return ans;
}
int cmp(char N2[], LL radix, LL t)
{
LL num = converNum10(N2, radix, t);
if(num < 0) return 1;
if(num == t) return 0;
else if(num < t) return -1;
else return 1;
}
LL binarySearch(char N2[], LL left, LL right, LL t)
{
LL mid;
while(left <= right)
{
mid = (left + right) / 2;
int flag = cmp(N2, mid, t);
if(flag == 0) return mid;
else if(flag == 1) right = mid - 1;
else left = mid + 1;
}
return -1;
}
int findLargestDigit(char N2[])
{
int ans = -1, len = strlen(N2);
for(int i = 0; i < len; i++)
{
if(ans < Map[N2[i]])
ans = Map[N2[i]];
}
return ans + 1;
}
int radix, tag;
char N1[20], N2[20], temp[20];
int main()
{
init();
scanf("%s %s %d %d", N1, N2, &tag, &radix);
if(tag == 2)
{
strcpy(temp, N1);
strcpy(N1, N2);
strcpy(N2, temp);
}
LL t = converNum10(N1, radix, inf);
LL left = findLargestDigit(N2);
LL right = max(left, t) + 1;
LL ans = binarySearch(N2, left, right, t);
if(ans == -1) printf("Impossible\n");
else printf("%lld\n", ans);
return 0;
}
A1044
本题也是可以用二分的方法,因为给定的数据不满足单调性,但是可以通过预处理使得数组元素变成累加和,这样即可满足单调性.
- 预处理以后求a[i] + … + a[j] 相当于求s[j] - s[i - 1]
- 第一遍循环设置一个变量nearD用来找最接近D的值对于每一个数据,找到第一个j使得s[j]>=s[i-1]+D,就相当于找到a[i]+…+a[j] >=d,s[j]-s[i-1]比变量nearD小,则更新nearD,nearD==D,则说明给定数据可以找到相加等于D的值,则直接退出.
- 再一次进行遍历,每一遍历哦用二分找到第一个j使得大于等于nearD + s[i-1]的值,如果是s[j]==nearD + s[i-1],等价于a[i]+…+a[j]==nearD,则进行输出即可.
#include<cstdio>
const int N = 100010;
int sum[N];
const int INF = (1 << 31) - 1;
int binary_search(int left, int right, int x)
{
int mid;
while(left < right)
{
mid = left + right >> 1;
if(sum[mid] >= x)
right = mid;
else
left = mid + 1;
}
return left;
}
int main()
{
int n, D;
scanf("%d%d", &n, &D);
sum[0] = 0;
for(int i = 1; i <= n; i++)
{
scanf("%d", &sum[i]);
sum[i] = sum[i] + sum[i - 1];
}
int Dnearst = INF;
for(int i = 1; i <= n; i++)
{
int j = binary_search(i, n + 1, D + sum[i - 1]);
if(sum[j] - sum[i - 1] < Dnearst && j <= n)
{
Dnearst = sum[j] - sum[i - 1];
}
if(Dnearst == D) break;
}
for(int i = 1; i <= n; i++)
{
int j = binary_search(i, n + 1, D + sum[i - 1]);
if(sum[j] - sum[i - 1] == Dnearst)
printf("%d-%d\n", i, j);
}
return 0;
}
A1048
本题是经典i + j == sum问题,可以用二分,hash,双指针来做,这里用二分来做.
- 首先对数组进行排序,保证有序来使用双指针.
- 因为是要找到某个值,所以用第一个查找是否存在的算法即可
- 要注意可能会出现i和j相等的情况,输出之前需要进行特判.
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 100010;
int a[N];
int binar_search(int left, int right, int x)
{
int mid;
while(left <= right)
{
mid = left + right >> 1;
if(a[mid] == x) return mid;
else if(a[mid] > x) right = mid - 1;
else left = mid + 1;
}
return -1;
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for(int i = 0; i < n; i++)
{
scanf("%d", &a[i]);
}
sort(a, a + n);
for(int i = 0; i < n; i++)
{
int j = binar_search(0, n - 1, m - a[i]);
if(j != -1 && i != j)
{
printf("%d %d", a[i], a[j]);
return 0;
}
}
printf("No Solution");
return 0;
}
二分法拓展
求21/2的近似值
const double eps = 1e-5;
double f(double x)
{
return x * x;
}
double calSqrt()
{
double left = 1, right = 2, mid;
while(right - left > eps)
{
mid = (left + right) / 2;
if(f[mid] > 2)
right = mid;
else
left = mid;
}
return mid;
}
上述问题是这样一个问题的特例: 给定一个定义再[L,R]上的单挑函数,求方程f(x) = 0的根
假设精度要求为eps = 10-5 ,函数递增,则可以根据left和right的中点mid与0的关系判断往哪个区间进行逼近.
- 如果f(mid) > 0,则说明根在mid左侧,所以应该往区间[left, mid]继续逼近,即left = mid.
- 如果f(mid) < 0,则说明根在mid右侧,所以应该往区间[mid, left]继续逼近,即令left = mid,
- 当righ - mid < eps时,说明达到精度要求,既可以返回当前的mid作为f(x)的根.
- 如果f(x)递减,则将f(mid) > 0 改成 f(mid) < 0即可.
模板:
const double eps = 1e-5;
double f(double x)
{
return ...;
}
double calSqrt()
{
double left = 1, right = 2, mid;
while(right - left > eps)
{
mid = (left + right) / 2;
if(f[mid] > 0)
right = mid;
else
left = mid;
}
return mid;
}
所以计算21/2 就相当于求f(x) = x2 - 2 = 0在[1, 2]的根.
快速幂
给定三个正数a,b,m(a < 109, b < 106, 1 < m <10 9),求ab % m
根据取模运算的一些基本性质:
- (a ± b) % p = (a % p ± b % p) % p
- (a * b) % p = (a % p * b % p) % p
- a ^ b % p = ((a % p)^b) % p
- (a+b) % p = ( a % p + b % p ) %p
可以用循环结合性质3快速写出代码:
typedef long long LL;
LLpow(LL a, LL b, LL m)
{
LL ans = 1;
for(int i = 0; i < b; i++)
ans = ans * a % m;
return ans;
}
但是如果b的范围变成 b < 1018 就不行了,这时我们使用快速幂做法.
快速幂基于二分思想,基于一下事实:
- 如果b是奇数,那么有 ab = a * ab-1.
- 如果b是偶数,那么有 ab = ab/2 * ab/2 .
如果b是奇数则总可以在下一步转换成b是偶数的情况,而b是偶数则在下一步可以转换成b/2的情况,所以根据log(b)次转换后b = 0
快速幂的递归写法:
typedef long long LL;
LL binarypow(LL a, LL b, LL m)
{
if(b == 0) return 1;
if(b % 2 == 1)
return a *binarypow(a, b - 1, m) % m;
else
{
LL temp = (a, b / 2, m);
return temp * temp % m;
}
}
注意点:
- 如果初值a大于m,则需要在进入函数前让a对m取模
- 如果m=1,则在函数外直接特判为0,因为任何数字对1取模都是0
- 条件if(b%2==1)可以用if(b&1)代替,因为b&1进行位操作,判断末位是否位1如果是1返回1,执行速度会快一点.
快速幂迭代思路:
思考可以得到如果把b写成二进制,例如4 = 4 + 1,那么a5 = a1+4 = a1 * a4 .
所以可以这样做
- 初始令ans = 1,用来存放累积结果.
- 判断b的二进制末尾是否为1,如果是令ans乘上a的值
- 令a平方,并将b右移一位
- 只要b大于0就返回2
快速幂递归代码:
typedef long long LL;
LL binarypow(LL a, LL b, LL m)
{
LL ans = 1;
while(b > 0)
{
if(b&1)
{
ans = ans * a % m;
}
a = a * a % m;
b >>= 1;
}
return ans % m;
}
4.6 双指针
双指针是利用问题本身与序列的特性,使用两个下标i,j对序列进行扫描(可以同向扫描,也可以反向扫描),以较低的复杂度解决问题.
应用实例:
1.sum和问题:
给定一个递增的整数序列和一个整数m,求序列中两个不同位置a,b使得他们的和恰好为M
这是经典的sum和问题,也是经典的双指针应用的场景,我们可以设置两个指针一个在头,一个在尾.每次相加判断值的大小并进行更新.
while(i < j)
{
if(a[i] + a[j] == m)
{
printf("%d %d", i, j);
i++;
j--;
}
else if(a[i] + a[j] > m)
j--;
else
i++;
}
双指针思想充分利用了序列递增的性质,时间复杂度为O(n).
2.序列合并问题:
假设两个递增序列A和B,要求合并他们为一个递增序列C
int merge(int A[], int B[], int C[], int n, int m)
{
int i = 0, j = 0, num = 0;
while(i < n && j < m)
{
if(A[i] <= B[j])
C[num++] = A[i++];
else
C[num++] = B[j++];
}
while(i < n) C[num++] = A[i++];
while(j < m) C[num++] = B[j++];
return num;
}
归并排序
归并排序是一种基于"归并"思想的排序方法,主要是二路归并排序.
二路归并排序原理是,将序列分成[n/2]个组(一个组有2个元素),每个组内进行排序后,再将这些组两两归并,生成[n/4]个组(每个组内有4个元素),对每个组再进行单独排序,直到只剩下一个组为止.
二路归并排序的核心在于 如何将两个有序序列合并为一个有序序列,这个问题可以用双指针的合并序列解决.
归并排序的时间复杂度O(nlogn)
二路归并排序的递归实现:
const int N = 100;
void merge(int A[], int L1,int R1, int L2, int R2)
{
int i = L1, j = L2;
int temp[N], num = 0;
while(i <= R1 && j <= R2)
{
if(A[i] <= A[j])
temp[num++] = A[i++];
else
temp[num++] = A[j++];
}
while(i <= R1) temp[num++] = A[i++];
while(j <= R2) temp[num++] = A[j++];
for(int i = 0, i < num; i++)
A[i + L1] = temp[i];
}
void mergeSort(int A[], int left, int right)
{
if(left < right)
{
int mid = left + right >> 1;
mergeSort(A, left, mid);
mergeSort(A, mid + 1, right);
merge(A, left, mid, mid + 1, right);
}
}
二路归并排序的非递归实现:
void mergeSort(int A[])
{
for(int step = 2; step / 2 < n; ste = step * 2)
{
for(int i = 0; i < n; i += step)
{
int mid = i + step / 2;
if(mid + 1 < n)
{
merge(A, i, mid, mid + 1, min(i + step - 1, n - 1));
}
}
}
}
step的含义是这一次归并完成后,一组内有step个元素.
如果题目只需要输出每一趟结束时的序列,那么可以用sort函数来代替
void mergeSort(int A[])
{
for(int step = 2; step / 2 < n; ste = step * 2)
{
for(int i = 0; i < n; i += step)
{
sort(A + i, A + min(i + step, n))
}
//此处用来输出某一趟结束时的序列
}
}
快速排序
对一个序列,调整元素中的位置使得A[1]左侧的元素都不超过A[1],A[1]右侧的元素都大于A[1],A[1]成为序列中的主元.
使用双指针算法可以最快的实现它
int Partition(int A[], int left, int right)
{
int temp = A[0];
while(left < right)
{
while(A[right] > temp && left < right) right--;
A[left] = A[right];
while(A[left] <= temp && left < right) left++;
A[right] = A[left];
}
A[left] = temp;
return temp;
}
快速排序的思路:
- 调整序列中的元素使得当前序列最左端的元素调整后满足左侧所有元素不超过该元素,右侧所有元素大于该元素.
- 对该元素的左侧和右侧分别递归进行1调整,直到当前调整的区间长度不超过1.
代码实现:
void quickSort(int A[], int left, int right)
{
if(left < right)
{
int pos = Partition(A, left, right);
quickSort(A, left, pos - 1);
quickSort(A, pos + 1, right);
}
}
快排的缺点时当元素接近有序的时候会达到最坏的时间复杂度O(n2).
主要原因: 没有把当前区间划分成两个长度接近的子区间
解决方法: 不总是选用A[left]作为主元,进行随机选择,这样虽然最坏时间复杂度不变,但是期望时间复杂度为O(nlogn)
C语言产生随机数
#include<cstdio>
#include<ctime>
#include<cstdlib>
int main()
{
srand((unsigned)time(NULL));
printf("%d", rand());
return 0;
}
- 由于rand()函数只能产生[0,RAND_MAX]范围内的整数(RAND_MAX)是stdlib.h里的一个常熟,如果想输出给定范围[a,b]内的随机数则需要使用:rand() % (b - a + 1)+a
- 对于1来说,这种做法只能生成左右端点不超过RAND_MAX的区间内,当生成大范围随机数时,我们可以先用随机数除以RAND_MAX,这样得到一个在[0.1]的浮点数,再用这个浮点是乘上(b - a + 1),再加上a即可
int p = (int)round(1.0*rand() / RAND_MAX * (right - left + 1) + left))
,round函数需要调用math.h函数
随机快排算法:
只需要对partition函数加两行即可,quicksort不需要变化
int Partition(int A[], int left, int right)
{
int p = round(1.0*rand() / RAND_MAX * (right - left + 1) + left);
swap(A[0], A[p])
int temp = A[0];
while(left < right)
{
while(A[right] > temp && left < right) right--;
A[left] = A[right];
while(A[left] <= temp && left < right) left++;
A[right] = A[left];
}
A[left] = temp;
return temp;
}
pat题目
A1085
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 100010;
int a[N];
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for(int i = 0; i < n; i++)
{
scanf("%d", &a[i]);
}
sort(a, a + n);
int ans = 0;
int i = 0, j = 0;
while(i < n)
{
while(a[j] <= (long long)a[i] * m && j < n) j++;
ans = max(ans, j - i);
i++;
}
printf("%d", ans);
return 0;
}
i和j同向扫描,因为随着i增大j也会增大.所以可以用双指针算法,每次更新ans即可.
A1089
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 110;
int a[N], tempa[N], b[N];
int n;
bool isSame(int A[], int B[])
{
for(int i = 0; i < n; i++)
if(A[i] != B[i]) return false;
return true;
}
void showArray(int A[])
{
for(int i = 0; i < n; i++)
{
printf("%d", A[i]);
if(i != n - 1) printf(" ");
}
printf("\n");
}
bool insertSort()
{
bool flag = false;
for(int i = 1; i < n; i++)
{
if(i != 1 && isSame(tempa, b))
flag = true;
int temp = tempa[i], j = i - 1;
while(j >= 0 && temp < tempa[j])
{
tempa[j + 1] = tempa[j];
j--;
}
tempa[j + 1] = temp;
if(flag == true)
return true;
}
return false;
}
void mergeSort()
{
bool flag = false;
for(int step = 2; step / 2 < n; step *= 2)
{
if(step != 2 && isSame(tempa, b))
flag = true;
for(int i = 0; i < n; i+=step)
sort(tempa + i, tempa + min(n, i + step));
if(flag == true)
return ;
}
}
int main()
{
scanf("%d", &n);
for(int i = 0; i < n; i++)
{
scanf("%d", &a[i]);
tempa[i] = a[i];
}
for(int i = 0; i < n; i++)
scanf("%d", &b[i]);
if(insertSort())
{
printf("Insertion Sort\n");
showArray(tempa);
}
else
{
for(int i = 0; i < n; i++)
tempa[i] = a[i];
printf("Merge Sort\n");
mergeSort();
showArray(tempa);
}
return 0;
}
A1029
本题是求两个序列的中位数,只需要利用归并排序的方法将找到第mid个小的数字即可.
需要注意可能会出现一个序列已经扫完但是还没到中位数的情况,这时可能会出现数据越界的情况,为了避免这种情况的发生,给每个序列的最后都添加一个很大的数字.
#include<cstdio>
const int N = 200010;
const int INF = (1 << 31) - 1;
int a[N], b[N];
int main()
{
int n, m;
scanf("%d", &n);
for(int i = 0; i < n; i++)
scanf("%d", &a[i]);
scanf("%d", &m);
for(int i = 0; i < m; i++)
scanf("%d", &b[i]);
a[n] = b[m] = INF;
int mid = n + m - 1 >> 1;
int i = 0, j = 0, cnt = 0;
while(cnt < mid)
{
if(a[i] < b[j]) i++;
else j++;
cnt++;
}
if(a[i] < b[j]) printf("%d", a[i]);
else printf("%d", b[j]);
return 0;
}
A1048
经典sum和问题.
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 100010;
int a[N];
int main()
{
int n, m;
bool flag = false;
scanf("%d%d", &n, &m);
for(int i = 0; i < n; i++)
scanf("%d", &a[i]);
sort(a, a + n);
int i = 0, j = n - 1;
while(i < j)
{
if(a[i] + a[j] == m)
{
printf("%d %d", a[i], a[j]);
flag = true;
break;
}
else if(a[i] + a[j] > m)
j--;
else
i++;
}
if(flag == false)
printf("No Solution");
return 0;
}
4.7 其他高效算法
活用递推
pat题目
A1093
本题和A1101思路类似,
- 先建立一个数组nump[n],求出每个字符的左边有多少个p
- 再从字符串最后进行遍历,并设置变量记录右边rightT的数目
- 如果当前字符是t,则更新变量rightT,如果当前字符是a,则更新ans
#include<cstdio>
#include<cstring>
const int N = 100010;
const int MOD = 1000000007;
char s[N];
int leftNumP[N] = {0};
int main()
{
scanf("%s", s);
int len = strlen(s);
for(int i = 0; i < len; i++)
{
if(i > 0) leftNumP[i] = leftNumP[i - 1];
if(s[i] == 'P') leftNumP[i]++;
}
int ans = 0, rightNumT = 0;
for(int i = len - 1; i >= 0; i--)
{
if(s[i] == 'T') rightNumT++;
else if(s[i] == 'A')
ans = (ans + rightNumT * leftNumP[i]) % MOD;
}
printf("%d\n", ans);
return 0;
}
A1101
注意最后要多输出一个换行,否则有数据格式错误
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 100010;
int a[N], minright[N], ans[N];
int main()
{
int n;
scanf("%d", &n);
for(int i = 0;i < n; i++)
scanf("%d", &a[i]);
int maxleft = -1, num = 0;
minright[n - 1] = a[n - 1];
for(int i = n - 2; i >= 0; i--)
{
if(a[i] < minright[i + 1])
minright[i] = a[i];
else
minright[i] = minright[i + 1];
}
for(int i = 0; i < n; i++)
{
maxleft = max(maxleft, a[i]);
if(a[i] >= maxleft && a[i] <= minright[i])
ans[num++] = a[i];
}
if(num == 0) printf("0\n");
else
{
sort(ans, ans + num);
printf("%d\n", num);
for(int i = 0; i < num; i++)
{
printf("%d", ans[i]);
if(i != num - 1) printf(" ");
}
}
printf("\n");
return 0;
}
随机选择算法
考虑这样一个问题:如何从一个无序的数组中求出第k小的数,虽然可以对数组排序然后取出第k个元素但是这样的做法需要O(nlogn)但是还有更高效的算法,随机选择算法 可以达到O(n)的期望时间复杂度.
随机选择算法原理与随机快速排序算法思想相同,当对A[left,right]执行partition函数以后,假设此时主元是A[p],那么A[p]就是A[left,right]里第p-left+1小的数字了,不妨令m表示p-left+1.
- 如果k == m成立,那么说明第k小的数字就是主元A[p]
- 如果k < m 成立,那么说明第k小的数在主元左侧,即A[left, p-1]中的第k小的数,往左递归即可.
- 如果k > m 成立,则说明第k大的主元在右侧,即A[(p+1), right]中的第k-m大,往右侧递归即可.
- 算法以left == right 为递归边界,返回A[left]
int randSelcetion(int A[], int left, int right, int k)
{
if(left == right) return A[left];
int p = randPartition(A, left, right);
int M = p - left + 1;
if(k == M) return A[p];
if(k < M) return randSelcetion(A, left, p - 1, k);
else return randSelcetion(A, k + 1, right, k - M);
}
虽然随机选择算法最快时间复杂度是O(n2),但是对任意输入期望时间复杂度是O(n).
实际问题应用: 给定一个由整数组成的集合,集合中的整数各不相同,现在要将它分为两个子集合,使得这两个子集合的并为原集合,交为空集,同时两个子集合的元素个数n1和n2之差的绝对值|n1-n2|尽可能小的前提下,他们各自元素之和s1与s2的绝对值|s1-s2|尽可能大,求|s1-s2|
分析: 如果原集合中的元素个数为n,那么当n是偶数时,两个子集合的元素个数都是n/2,当n时奇数时,两个子集合的元素分别时n/2,(n+1)/2.为使|s2-s1|最大,则将元素从小到大排序,取前n/2为第一个元素子集合,剩下为另一个子集合即可.
本质就是上面介绍的随机选择算法.求第n/2小的数字,因为randpartition会自动将数组分好,所以直接累加即可.
#include<cstdio>
#include<algorithm>
#include<ctime>
#include<cmath>
using namespace std;
const int N = 1110;
int a[N];
int randPartition(int A[], int left, int right)
{
int p = round(1.0*rand()/RAND_MAX*(right - left) + left);
swap(A[left], A[p]);
int temp = A[left];
while(left < right)
{
while(left < right && A[right] > temp) right--;
A[left] = A[right];
while(left < right && A[left] <= temp) left++;
A[right] = A[left];
}
A[left] = temp;
return left;
}
void randSelect(int A[], int left, int right, int k)
{
if(left == right) return ;
int p = randPartition(A, left, right);
int m = p - left + 1;
if(k == m) return;
if(k < m) return randSelect(A, left, p - 1, k);
if(k > m) return randSelect(A, p + 1, right, k - m);
}
int main()
{
int n;
scanf("%d", &n);
for(int i = 0; i < n; i++)
scanf("%d", &a[i]);
int mid = n / 2;
randSelect(a, 0, n - 1, mid);
for(int i = 0; i < n; i++)
printf("%d ", a[i]);
printf("\n");
int sum1 = 0, sum = 0;;
for(int i = 0; i < mid; i++)
sum1 += a[i];
for(int i = 0; i < n; i++)
sum += a[i];
printf("%d\n", (sum - sum1) - sum1);
return 0;
}