本文是我对第八章19道例题的练习总结,建议配合紫书——《算法竞赛入门经典(第2版)》阅读本文。
另外为了方便做题,我在VOJ上开了一个contest,欢迎一起在上面做:第八章例题contest
如果想直接看某道题,请点开目录后点开相应的题目!!!
题意
有一叠煎饼正在锅里。煎饼共有n(n≤30)张,每张都有一个数字,代表它的大小。厨师每次可以选择一个数k,把从锅底开始数第k张上面的煎饼全部翻过来,即原来在上面的煎饼现在到了下面。
设计一种方法使得所有煎饼按照从小到大排序(最上面的煎饼最小)。输入时,各个煎饼按照从上到下的顺序给出。
思路
这道题目要求排序,但是基本操作却是“颠倒一个连续子序列”。不过没有关系,我们还是可以按照选择排序的思想,以从大到小的顺序依次把每个数排到正确的位置。方法是先翻到最上面,然后翻到正确的位置。由于是按照从大到小的顺序处理,当处理第i大的煎饼时,是不会影响到第1, 2, 3,…, i-1大的煎饼的(它们已经正确地翻到了煎饼堆底部的i-1个位置上)。
开始马虎了,将if (j > 1)判断条件不慎写成了if (j < 1)提交后WA了两发,提醒自己一定要细心啊!
代码
#include<cstdio>
#include<cstring>
#include<string>
#include<iostream>
#include<sstream>
#include<algorithm>
#include<vector>
using namespace std;
int n;
int a[31];
void flip(int k)
{
for (int i = 1; i <= k/2; i++)
swap(a[i], a[k-i+1]);
}
int main()
{
string s;
//freopen("in", "r", stdin);
while (getline(cin, s)) {
stringstream ss(s);
int x;
n = 0;
while (ss >> x)
a[++n] = x;
int b[31];
memcpy(b, a, sizeof(a));
sort(b+1, b+n+1);
vector<int> res;
for (int i = n; i >= 1; i--) {
if (a[i] != b[i]) {
int j = 1;
for (; a[j] != b[i]; j++);
if (j > 1) {flip(j); res.push_back(n-j+1);}
flip(i); res.push_back(n-i+1);
}
}
cout << s << endl;
for (int i = 0; i < res.size(); i++)
printf("%d ", res[i]);
printf("0\n");
}
return 0;
}
题意
你的任务是设计一个包含若干层的联合国大楼,其中每层都是一个等大的网格。有若干国家需要在联合国大楼里办公,你需要把每个格子分配给一个国家,使得任意两个不同的国家都有一对相邻的格子(要么是同层中有公共边的格子,要么是相邻层的同一个格子)。你设计的大厦最多不能超过1000000个格子。
输入国家的个数n(n≤50),输出大楼的层数H、每层楼的行数W和列数L,然后是每层楼的平面图。不同国家用不同的大小写字母表示。
思路
本题的限制非常少,层数、行数和列数都可以任选。正因为如此,本题的解法非常多。我采用的是书中给出的解法:一共只有两层,每层都是n*n的,第一层第i行全是国家i,第二层第j列全是国家j。
代码
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
char ans[]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
int main()
{
int n;
while (scanf("%d", &n) != EOF){
printf("2 %d %d\n", n, n);
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++)
printf("%c", ans[i]);
printf("\n");
}
printf("\n");
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++)
printf("%c", ans[j]);
printf("\n");
}
}
return 0;
}
题意
给定4个n(1≤n≤4000)元素集合A, B, C, D,要求分别从中选取一个元素a, b, c, d,使得
a+b+c+d=0。问:有多少种选法?
思路
中途相遇法。这是一种特殊的算法,大体思路是从两个不同的方向来解决问题,最终“汇集”到一起。
最容易想到的算法就是写一个四重循环枚举a, b, c, d,看看加起来是否等于0,时间复杂度为O(n4),超时。一个稍好的方法是枚举a, b, c,则只需要在集合D里找找是否有元素-a-bc,如果存在,则方案加1。如果排序后使用二分查找,时间复杂度为(n3logn)。
把刚才的方法加以推广,就可以得到一个更快的算法:首先枚举a和b,把所有a+b记录下来放在一个有序数组或者STL的map里,然后枚举c和d,查一查-c-d有多少种方法写成a+b的形式。两个步骤都是O(n2logn),总时间复杂度也是O(n2logn)。
代码
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 4000;
int n, m;
int a[4][N];
int x[N*N], y[N*N];
int main(void)
{
int kase;
cin >> kase;
while (kase--) {
cin >> n;
m = n*n;
for (int i = 0; i < n; i ++)
for (int j = 0; j < 4; j ++)
scanf("%d", &a[j][i]);
for (int i = 0; i < n; i ++)
for (int j = 0; j < n; j ++)
x[i*n+j] = a[0][i] + a[1][j];
for (int i = 0; i < n; i ++)
for (int j = 0; j < n; j ++)
y[i*n+j] = a[2][i] + a[3][j];
sort(y, y+m);
long long ans = 0;
for (int i = 0; i < m; i ++)
ans += (upper_bound(y, y+m, -x[i]) - lower_bound(y, y+m, -x[i]));
printf("%lld\n", ans);
if (kase) printf("\n");
}
return 0;
}
题意
你的任务是在n*n的棋盘上放n(n≤5000)个车,使得任意两个车不相互攻击,且第i个车在一个给定的矩形Ri之内。用4个整数xli, yli, xri, yri(1≤xli≤xri≤n,1≤yli≤yri≤n)描述第i个矩形,其中(xli,yli)是左上角坐标,(xri,yri)是右下角坐标,则第i个车的位置(x,y)必须满足xli≤x≤xri,yli≤y≤yri。如果无解,输出IMPOSSIBLE;否则输出n行,依次为第1,2,…,n个车的坐标。
思路
两个车相互攻击的条件是处于同一行或者同一列,因此不相互攻击的条件就是不在同一行,也不在同一列。可以看出:行和列是无关的,因此可以把原题分解成两个一维问题。
在区间[1~n]内选择n个不同的整数,使得第i个整数在闭区间[n1i, n2i]内。贪心法可解。复杂度O(n^2).
代码
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<queue>
using namespace std;
const int N = 5001;
int n;
int rangeA[2][N], rangeB[2][N];
int pos[2][N];
bool put_rook(int i)
{
int *ra = rangeA[i], *rb = rangeB[i];
int *p = pos[i];
int c[N];
memset(c, 0, sizeof(c));
for (int j = 1; j <= n; j++) {
int mink = n+1, minb = n+1;
for (int k = 1; k <= n; k++) {
if (!c[k] && ra[k] <= j && rb[k] >= j && rb[k] < minb) {
mink = k;
minb = rb[k];
}
}
//printf("j=%d, mink=%d, ra[mink]=%d, rb[mink]=%d\n", j, mink, ra[mink], rb[mink]);
if (mink == n+1) return false;
p[mink] = j;
c[mink] = 1;
}
return true;
}
int main()
{
while (scanf("%d", &n) && n) {
for (int i = 1; i <= n; i++) {
for (int j = 0; j < 2; j++)
scanf("%d", &rangeA[j][i]);
for (int j = 0; j < 2; j++)
scanf("%d", &rangeB[j][i]);
}
if (!put_rook(0) || !put_rook(1)) printf("IMPOSSIBLE\n");
else {
for (int i = 1; i <= n; i++)
printf("%d %d\n", pos[0][i], pos[1][i]);
}
}
return 0;
}
题意
直线上有n(2≤n≤100000)个等距的村庄,每个村庄要么买酒,要么卖酒。设第i个村庄对酒的需求为ai(-1000≤ai≤1000),其中ai>0表示买酒,ai<0表示卖酒。所有村庄供需平衡,即所有ai之和等于0。
把k个单位的酒从一个村庄运到相邻村庄需要k个单位的劳动力。计算最少需要多少劳动力可以满足所有村庄的需求。输出保证在64位带符号整数的范围内。
思路
考虑最左边的村庄。如果需要买酒,即a1>0,则一定有劳动力从村庄2往左运给村庄1,而不管这些酒是从哪里来的(可能就是村庄2产的,也可能是更右边的村庄运到村庄2的)。这样,问题就等价于只有村庄2~n,且第2个村庄的需求为a1+a2的情形。不难发现,ai<0时这个推理也成立(劳动力同样需要|ai|个单位).
代码
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
int main( ) {
int n;
while(cin >> n && n) {
long long ans = 0, a, last = 0;
for(int i = 0; i < n; i++) {
cin >> a;
ans += abs(last);
last += a;
}
cout << ans << "\n";
}
return 0;
}
题意
思路
代码
题意
输入一个长度为n(n≤106)的序列A,找到一个尽量长的连续子序列AL~AR,使得该序列中没有相同的元素。
思路
假设序列元素从0开始编号,所求连续子序列的左端点为L,右端点为R。首先考虑起点L=0的情况。可以从R=0开始不断增加R,相当于把所求序列的右端点往右延伸。当无法延伸(即A[R+1]在子序列A[L~R]中出现过)时,只需增大L,并且继续延伸R。既然当前的A[L~R]是可行解,L增大之后必然还是可行解,所以不必减少R,继续增大即可。
不难发现这个算法是正确的,不过真正有意思的是算法的时间复杂度。暂时先不考虑“判断是否可以延伸”这个部分,每次要么把R加1,要么把L加1,而L和R最多从0增加到n-1,所以指针增加的次数是O(n)的。
最后考虑“判断是否可以延伸”这个部分。比较容易想到的方法是用一个STL的set,保存A[L~R]中元素的集合,当R增大时判断A[R+1]是否在set中出现,而R加1时把A[R+1]插入到set中,L+1时把A[L]从set中删除。因为set的插入删除和查找都是O(logn)的,所以这个算法的时间复杂度为O(nlogn)。
代码
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<set>
using namespace std;
const int N = 1000001;
int n;
int a[N];
int main()
{
int kase;
scanf("%d", &kase);
while (kase--) {
scanf("%d", &n);
for (int i = 0; i < n; i++)
scanf("%d", &a[i]);
int ans = 1, l = 0, r = 0;
set<int> sa;
while (true) {
while (r < n && !sa.count(a[r]))
sa.insert(a[r++]);
ans = max(ans, r-l);
if (r == n) break;
sa.erase(a[l++]);
}
printf("%d\n", ans);
}
return 0;
}
题意
思路
代码
题意
思路
代码
题意
把一个包含m个正整数的序列划分成k个(1≤k≤m≤500)非空的连续子序列,使得每个正整数恰好属于一个序列。设第i个序列的各数之和为S(i),你的任务是让所有S(i)的最大值尽量小。例如,序列1 2 3 2 5 4划分成3个序列的最优方案为1 2 3 | 2 5 | 4,其中S(1)、S(2)、S(3)分别为6、7、4,最大值为7;如果划分成1 2 | 3 2 | 5 4,则最大值为9,不如刚才的好。每个整数不超过107。如果有多解,S(1)应尽量小。如果仍然有多解,S(2)应尽量小,依此类推。
思路
“最大值尽量小”是一种很常见的优化目标。下面考虑一个新的问题:能否把输入序列划分成m个连续的子序列,使得所有S(i)均不超过x?将这个问题的答案用谓词P(x)表示,则让P(x)为真的最小x就是原题的答案。P(x)并不难计算,每次尽量往右划分即可(想一想,为什么)。
接下来又可以猜数字了——随便猜一个x0,如果P(x0)为假,那么答案比x0大;如果P(x0)为真,则答案小于或等于x0。至此,解法已经得出:二分最小值x,把优化问题转化为判定问题P(x)。设所有数之和为M,则二分次数为O(logM),计算P(x)的时间复杂度为O(n)(从左到右扫描一次即可),因此总时间复杂度为O(nlogM)(4)。
此题值得注意的地方是如何使得越靠前的划分值越小。我的实现方法是将其转化为从后往前搜索,使得越靠后的划分的划分值越大。详见代码。
代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 501;
const int INF = 10000000;
typedef long long LL;
int n, k;
int a[N];
bool check(LL mid)
{
int cnt = 1;
LL sum = 0;
for (int i = 0; i < n; i++) {
if (a[i] > mid) return false;
if (sum + a[i] > mid) {
cnt++;
sum = a[i];
} else
sum += a[i];
}
return cnt <= k;
}
void get_div(vector<int>& div, LL mid)
{
int cnt = 1;
LL sum = 0;
for (int i = n-1; i >= 0; i--) {
if (i < k-cnt || sum + a[i] > mid) {
cnt++;
sum = a[i];
div.push_back(i+1);
} else
sum += a[i];
}
}
int main(void)
{
int kase;
cin >> kase;
while (kase--) {
cin >> n >> k;
LL sum = 0;
for (int i = 0; i < n; i++) {
scanf("%d", &a[i]);
sum += a[i];
}
LL lb = 0, ub = sum;
while (ub - lb > 1) {
LL mid = (lb + ub) / 2;
if (check(mid)) ub = mid;
else lb = mid;
}
vector<int> div;
get_div(div, ub);
int j = k-2;
for (int i = 0; i < n; i++) {
if (div[j] == i) { printf("/ "); j--;}
printf("%d%c", a[i], i == n-1 ? '\n' : ' ');
}
}
return 0;
}
题意
有n(n≤5000)个数的集合S,每次可以从S中删除两个数,然后把它们的和放回集合,直到剩下一个数。每次操作的开销等于删除的两个数之和,求最小总开销。所有数均小于10^5。
思路
这不就是Huffman编码的建立过程吗?因为n比较小,还可以采用一种更容易写的方法——使用一个优先队列。
代码
#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
using namespace std;
int main(void)
{
int n;
while(cin >> n && n)
{
priority_queue<int, vector<int>, greater<int> > Queue;
for(int i = 1; i <= n; i++)
{
int temp;
scanf("%d", &temp);
Queue.push(temp);
}
int mincost = 0;
while (Queue.size() > 1)
{
int a = Queue.top();
Queue.pop();
int b = Queue.top();
Queue.pop();
Queue.push(a+b);
mincost += a+b;
}
printf("%d\n", mincost);
while(!Queue.empty())
Queue.pop();
}
return 0;
}
题意
一开始有一个红气球。每小时后,一个红气球会变成3个红气球和一个蓝气球,而一个蓝气球会变成4个蓝气球,如图8-19所示分别是经过0, 1, 2, 3小时后的情况。经过k小时后,第A~B行一共有多少个红气球?例如,k=3,A=3,B=7,答案为14。
思路
k小时的情况由4个k-1小时的情况拼成,其中右下角全是蓝气球,不用考虑。剩下的3个部分有一个共同点:都是前k-1小时后“最下面若干行”或者“最上面若干行”的红气球总数。
具体来说,设f(k, i)表示k小时之后最上面i行的红气球总数,g(k,i)表示k小时之后最下面i行的红气球总数(规定i≤0时f(k,i)=g(k,i)=0),则所求答案为f(k,b) - f(k, a-1)。
如何计算f(k,i)和g(k,i)呢?以g(k,i)为例,下面分两种情况进行讨论.
如果i≥2k-1,则g(k,i)=2g(k-1,i-2k-1)+c(k),否则g(k,i)=g(k-1,i)。其中,c(k)表示k小时后红气球的总数,满足递推式c(k)=3c(k-1),而c(0)=1,因此c(k)=3k。
不管是哪种情况,g(k,i)都可以直接转化为k-1的情况,因此g(k,i)的计算时间为O(k)。类似地,f(k,i)的计算时间也是O(k),因此本题的总时间复杂度为O(k)。
我开始的做法是直接递归求解f(k, a, b),也就是k小时后a行与b行之间的红气球总数。递推式大概是f(k, a, b) = 2*f(k-1, a1, b1) + f(k-1, a2, b2)的样子,一提交TLE了。分析了一下,这个递推式的时间复杂度是O(2^k)的,远远超出线性复杂度!然后改成书中提到的递归方式就AC了。
代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
int exp2[31];
LL exp3[31];
LL f1(int k, int a) //up
{
if (a == 0) return 0;
if (k == 0) return 1;
if (a <= exp2[k-1]) return 2*f1(k-1, a);
else return 2*exp3[k-1] + f1(k-1, a-exp2[k-1]);
}
LL f2(int k, int b) //up
{
if (b == exp2[k]+1) return 0;
if (k == 0) return 1;
if (b > exp2[k-1]) return f2(k-1, b-exp2[k-1]);
else return exp3[k-1] + 2*f2(k-1, b);
}
int main(void)
{
exp2[0] = exp3[0] = 1;
for (int i = 1; i <= 30; i++) {
exp2[i] = exp2[i-1]*2;
exp3[i] = exp3[i-1]*3;
}
int kase;
scanf("%d", &kase);
for (int t = 1; t <= kase; t++) {
int k, a, b;
scanf("%d%d%d", &k, &a, &b);
printf("Case %d: %lld\n", t, exp3[k] - f1(k, a-1) - f2(k, b+1));
}
return 0;
}
题意
环形跑道上有n(n≤100000)个加油站,编号为1~n。第i个加油站可以加油pi加仑。从加油站i开到下一站需要qi加仑汽油。你可以选择一个加油站作为起点,初始油箱为空(但可以立即加油)。你的任务是选择一个起点,使得可以走完一圈后回到起点。假定油箱中的油量没有上限。如果无解,输出Not possible,否则输出可以作为起点的最小加油站编号。
思路
考虑1号加油站,直接模拟判断它是否为解。如果是,直接输出;如果不是,说明在模拟的过程中遇到了某个加油站p,在从它开到加油站p+1时油没了。这样,以2, 3,…, p为起点也一定不是解(想一想,为什么)。这样,使用简单的枚举法便解决了问题,时间复杂度为O(n)。
代码
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100001;
int n;
int p[N], q[N];
int main()
{
int kase;
scanf("%d", &kase);
for (int t = 1; t <= kase; t++) {
scanf("%d", &n);
for (int i = 0; i < n; i++)
scanf("%d", &p[i]);
for (int i = 0; i < n; i++)
scanf("%d", &q[i]);
int bg = 0;
while (bg < n) {
int i = bg, sp = 0, sq = 0;
for (; i < bg+n; i++) {
sp += p[i%n], sq += q[i%n];
if (sp < sq) break;
}
if (i == bg+n) break;
bg = i+1;
}
printf("Case %d: ", t);
if (bg < n) printf("Possible from station %d\n", bg+1);
else printf("Not possible\n");
}
return 0;
}
题意
可以用与非门(NAND)来设计逻辑电路。每个NAND门有两个输入端,输出为两个输入端与非运算的结果。即输出0当且仅当两个输入都是1。给出一个由m(m≤200000)个NAND组成的无环电路,电路的所有n个输入(n≤100000)全部连接到一个相同的输入x。
请把其中一些输入设置为常数,用最少的x完成相同功能。输出任意方案即可。
思路
因为只有一个输入x,所以整个电路的功能不外乎4种:常数0、常数1、x及非x。先把x设为0,再把x设为1,如果二者的输出相同,整个电路肯定是常数,任意输出一种方案即可。
如果x=0和x=1的输出不同,说明电路的功能是x或者非x,解至少等于1。不妨设x=0时输出0,x=1时输出1。现在把第一个输入改成1,其他仍设为0(记这样的输入为1000…0),如果输出是1,则得到了一个解x000…0。
如果1000…0的输出也是0,再把输入改成1100…0,如果输出是1,则又得到了一个解1x00…0。如果输出还是0,再尝试1110…0,如此等等。由于输入全1时输出为1,这个算法一定会成功。
问题在于m太大,而每次“给定输入计算输出”都需要O(m)时间,逐个尝试会很慢。好在已经学习了二分查找:只需二分1的个数,即可在O(Logm)次计算之内得到结果,总时间复杂度为O(mlogm)。
如果电路输出非常数,则一定有一个关键的x能够决定结果。问题在于找到这个关键的x。二分法可解。
这个题需要加深理解,目前我还没有完全理解透。
代码
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100001;
const int M = 200001;
int n, m;
bool in[N], out[M];
int gate[M][2];
bool get_out(int k)
{
fill(in+1, in+k+1, 1);
fill(in+k+1, in+n+1, 0);
bool a, b;
for (int i = 1; i <= m; i++) {
a = gate[i][0] < 0 ? in[-gate[i][0]] : out[gate[i][0]];
b = gate[i][1] < 0 ? in[-gate[i][1]] : out[gate[i][1]];
out[i] = !(a&&b);
}
return out[m];
}
void print(int posx)
{
for (int i = 1; i < posx; i++)
printf("1");
if (posx) printf("x");
for (int i = posx+1; i <= n; i++)
printf("0");
printf("\n");
}
int main()
{
int kase;
scanf("%d", &kase);
while (kase--) {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++)
scanf("%d%d", &gate[i][0], &gate[i][1]);
int posx = 0;
int res[2];
res[0] = get_out(0);
res[1] = get_out(n);
if (res[0] != res[1]) {
int lb = 0, ub = n;
while (ub - lb > 1) {
int mid = (lb + ub) / 2;
if (get_out(mid) == res[1]) ub = mid;
else lb = mid;
}
posx = ub;
printf("%d\n", ub);
}
print(posx);
}
return 0;
}
题意
你正在使用的音乐播放器有一个所谓的乱序功能,即随机打乱歌曲的播放顺序。假设一共有s首歌,则一开始会给这s首歌随机排序,全部播放完毕后再重新随机排序、继续播放,依此类推。注意,当s首歌播放完毕之前不会重新排序。这样,播放记录里的每s首歌都是1~s的一个排列。
给出一个长度为n(1≤s,n≤100000)的播放记录(不一定是从最开始记录的)xi(1≤xi≤s),你的任务是统计下次随机排序所发生的时间有多少种可能性。
例如,s=4,播放记录是3, 4, 4, 1, 3, 2, 1, 2, 3, 4,不难发现只有一种可能性:前两首是一个段的最后两首歌,后面是两个完整的段,因此答案是1;当s=3时,播放记录1, 2, 1有两种可能:第一首是一个段,后两首是另一段;前两首是一段,最后一首是另一段。答案为2。
思路
“连续的s个数”让你联想到了什么?没错,滑动窗口!这次的窗口大小是“基本”固定的(因为还需要考虑不完整的段),因此只需要一个指针;而且所有数都是1~s的整数,也不需要STL的set,只需要一个数组即可保存每个数在窗口中出现的次数。再用一个变量记录在窗口中恰好出现一次的数的个数,则可以在O(n)时间内判断出每个窗口是否满足要求(每个整数最多出现一次)。
这样,就可以枚举所有可能的答案,判断它对应的所有窗口,当且仅当所有窗口均满足要求时这个答案是可行的。
此题的思路非常明确,但在实际编码中却没那么顺利。s和n不一定那个大那个小,滑动窗口的大小有可能是小于s的,要考虑到各种情况,很容易漏考虑了某种情况导致WA。
代码不解释了,给出一组测试数据帮没有AC的人:
INPUT
1
6 4
1 4 6 4
OUTPUT
2
代码
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100001;
int s, n;
int a[N], c[N];
bool flag[N];
int main()
{
int kase;
scanf("%d", &kase);
while (kase--) {
scanf("%d%d", &s, &n);
for (int i = 0; i < n; i++)
scanf("%d", &a[i]);
memset(c, 0, sizeof(c));
memset(flag, true, sizeof(flag));
int num = 0;
for (int i = 0; i < n+s; i++) {
if (i < n && !(c[a[i]]++)) num++;
if (i >= s && !(--c[a[i-s]])) num--;
if (i < s) {
if (num < min(n, i+1)) flag[i%s] = false;
} else if (i < n) {
if (num < s) flag[i%s] = false;
} else if (num < n-max(i-s+1, 0)) {
flag[i%s] = false;
}
}
num = 0;
for (int i = 0; i < s; i++)
if (flag[i]) num++;
printf("%d\n", num);
}
return 0;
}
题意
如果一个序列的任意连续子序列中至少有一个只出现一次的元素,则称这个序列是不无聊(non-boring)的。输入一个n(n≤200000)个元素的序列A(各个元素均为109以内的非负整数),判断它是不是不无聊的。
思路
不难想到整体思路:在整个序列中找一个只出现一次的元素,如果不存在,则这个序列不是不无聊的;如果找到一个只出现一次的元素A[p],则只需检查A[1…p-1]和A[p+1…n]是否满足条件。
如何找唯一元素?如果事先算出每个元素左边和右边最近的相同元素(还记得《唯一的雪花》吗?),则可以在O(1)时间内判断在任意一个连续子序列中,某个元素是否唯一。
但从左往右找和从右往左找的最坏情况下时间复杂度是O(n^2),而从两边往中间找的时间复杂度则为O(nlogn),详细分析见书中。
代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <map>
using namespace std;
const int N = 200001;
int n;
int a[N], lnext[N], rnext[N];
bool check(int l, int r)
{
if (r-l < 1) return true;
for (int i = 0; i <= (r-l)/2; i++) {
int ll = l+i;
if (lnext[ll] < l && rnext[ll] > r)
if (check(l, ll-1) && check(ll+1, r)) return true;
int rr = r-i;
if (rr == ll) break;
if (lnext[rr] < l && rnext[rr] > r)
if (check(l, rr-1) && check(rr+1, r)) return true;
}
return false;
}
int main(void)
{
int kase;
scanf("%d", &kase);
for (int t = 1; t <= kase; t++) {
scanf("%d", &n);
map<int, int> mp;
mp.clear();
for (int i = 0; i < n; i++) {
scanf("%d", &a[i]);
lnext[i] = mp.count(a[i]) ? mp[a[i]] : -1;
mp[a[i]] = i;
}
mp.clear();
for (int i = n-1; i >= 0; i--) {
rnext[i] = mp.count(a[i]) ? mp[a[i]] : n;
mp[a[i]] = i;
}
if (check(0, n-1)) printf("non-boring\n");
else printf("boring\n");
}
return 0;
}
题意
思路
代码
题意
思路
代码
题意
思路
代码