常用技巧精选(一)——《挑战程序设计竞赛》

3.2 常用技巧精选(一)

3.2.1 尺取法

Subsequence

给出N个数字:,每个数字不大于10000,给出一个整数S,在N个数字中挑选出连续子序列,使这个子序列和大于或等于S。请问这个连续的子序列长度的最小值。

我们设以开始总和最初大于S时的连续子序列为,这时

所以从$a_{s+1}$开始总和最初超过S的连续子序列如果是$a_{s+1}+\cdots+a_{t\prime-1}$的话,必然有$t\le t\prime$。利用这一性质便可以设计如下算法:
(1) 以s=t=sum=0初始化。
(2) 只要依然有sum<S,就不断将sum增加$a_t$,并将t增加1.
(3) 如果(2)中无法满足sum$\ge$S则终止。否则的话,更新res=min(res,t-s)。
(4) 将sum减去$a_s$,s增加1然后回到(2)。

对于这个算法,因为t最多变化n次,因此只需O(n)的复杂度就可以求解这个问题了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include<iostream>
using namespace std;
int a[100010];
int main()
{
int t;
cin>>t;
while(t-->0)
{
int n,S;
cin>>n>>S;
for(int i=0;i<n;i++)
{
cin>>a[i];
}
int res = n + 1;
int s = 0, t = 0, sum = 0;
for(;;)
{
while(t<n && sum<S)
{
sum += a[t++];
}
if(sum < S) break;
res = min(res,t-s);
sum -= a[s++];
}
if(res > n)
{
res = 0;
}
cout<<res<<endl;
}

}

3.2.2 反转(开关问题)

Face The Right Way

N头牛排成一列,头要么朝前要么朝后,现在要求确定一个连续反转牛头的连续区间,区间长度为K,要使得所有牛都朝前,且反转次数M尽可能小。求出最小的操作数M和对应的最小的K。

首先,交换区间反转顺序的先后对结果毫无影响。此外,可以知道对同一个区间进行两次以上的反转是多余的,由此,问题就转化成了求需要被反转的区间的集合。于是我们先考虑一下最左端的牛。包含这头牛的区间只有一个,因此如果这头牛面朝前方,我们就能知道这个区间不需要反转。反之,如果这头牛面朝后方,对应的区间就必须进行反转了。而且在此之后这个最左的区间就再也不需要考虑了。这样一来,通过首先考虑最左端的牛,问题的规模就缩小了1。不断的重复下去,就可以无需搜索求出最少所需的反转次数了。

然而,我们需要遍历K,对于每个K我们都要从最左端开始考虑N头牛的情况,最坏情况需要进行N-K+1次的反转操作,而每次操作又要反转K头牛,所以总的复杂度是$O(N^3)$。

对于区间反转部分进行优化:优化的方法是计算第i头牛是否要翻转的时候,只需要知道第i-k+1头到第i头之间的翻转次数,那么维护这个次数即可。

这样,在考虑第i头年时,如果$\sum_{j=i-K+1}^{i-1}f[j]$为奇数的话,则这头牛的方向与起始方向是相反的,否则方向不变。依据如下公式

使用尺取法,每次向右移动一格,需要看看左边出去的那格(第i-k格)是翻转了没有,维护好f数组即可。这样扫一遍的复杂度是$O(n)$,那么总复杂度就是$O(n^2)$。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
const int M = 5100;
int n;
int dir[M];
int f[M];

int calc(int K)
{
memset(f,0,sizeof(f));
int res = 0;
int sum = 0; //当前维护的f的和
for(int i=0;i+K<=n;i++)
{
if((dir[i]+sum) % 2 != 0)
//最前端的牛面朝后方
{
res++;
f[i] = 1;
}
sum += f[i];
if(i-K+1>=0)
{
sum -= f[i-K+1];
}
}
// 检查剩下的牛,从n-K+1开始,是否有面朝后方的情况
for(int i=n-K+1;i<n;i++)
{
if((dir[i]+sum)%2!=0) //无解
{
return -1;
}
if(i-K+1>=0)
sum-=f[i-K+1];
}
return res;
}

int main()
{
cin>>n;
for(int i=0;i<n;i++)
{
char c;
cin>>c;
if(c=='B')
{
dir[i]=1;
}
else
{
dir[i]=0;
}
}
solve();
return 0;
}

Fliptile

有一个m*n的棋盘,每个格子上是0或1,每次可以对一个格子做一次翻转操作,将被操作的格子和上下左右4个格子的0/1翻转。问做少做多少次翻转可以将所有格子翻转成0,输出翻转方案。没有方案时输出“IMPOSSIBLE”。
$1\le m, n \le 15$

回顾一下前情,在之前POJ3276问题中,让最左边的牛反转的方法只有1种,可以直接判断当前的方案是否可行。然而,同样的方法在此题中不能行得通。不妨看最左上角的格子,这里除了翻转(1,1)之外,翻转(1,2)和(2,1)都可以把这个格子翻转,所以之前的直接确定的方法在此行不通。

值得注意的一点是这题中的m,n特别小,简直就是在明示可以有某种枚举的方法。因为本题格子间的状态都是互相影响的,只能通过枚举第一行,逐行往下搜,如何搜索:如果从上到下搜索,当前行是否需要反转取决于上一行的状态,通过翻转当前行使上一行为0,而不是通过上一行翻转为0后,看当前行的状态判断自己是否需要翻转,否则还会继续影响上一行。意思就是不是在当前行中的翻转操作不是为了让当前行中的所有格子都为0,而是要让上一行的所有格子都为0。所以我们可以通过枚举第一行所有的状态,从第二行开始确定翻转状态,直到最后一行结束,如果可以保证最后一行都是0,那么方案可行,否则重新定义第一行的状态,继续搜索,找出使反转次数最少的方案。

3.2.4 折半枚举(双向搜索)

超大背包问题
有重量和价值分别为$w_i, v_i$的n个物品,从这些物品中挑选总重量不超过W的物品,求所有挑选方案中价值总和的最大值
限制条件:$1 \le n \le 40, 1\le w_i, v_i \le 10^{15}, 1 \le W \le 10^{15} $

所谓超大背包问题,W的最大值是$10^15$,使用DP来求解背包问题的复杂度是$O(nW)$,因此不能解决这里的问题。但是n比较小,依此寻求枚举解法。

从所有物品里挑选的全排列的数量为$2^n$中,在这里虽然n很小,但是$2^40$依然顶不住,所以我们想到折半枚举,把物品拆成两半再枚举。我们把前半部分中枚举出的重量和价值总和记为$w_1、 v_1$。这样在后半部分寻找总重$w_2\le W-w_1$时使$v_2$最大的选取方法就好了。

接着我们考虑从枚举得到的$(w_2,v_2)$的集合中高效寻找$max{v_2|w_2 \le W\prime}$的方法。在代码中当然使用pair这个数据结构来维护重量和价值对,那么我们根据重量排序之后可以排除所有$w_2[i] \le w_2[j]$并且$v_2[i] \ge v_2[j]$的j。接下来只要寻找$w_2[i] \le W\prime$的最大的i就可以了。使用二分搜索完成,剩余元素的个数为M($M\le 2^{(n/2)}$)的话,一次搜素需要$O(\log M)$,所以这个算法的总复杂度是$O(2^{(n/2)}n)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
typedef long long ll;
//输入
int n;
ll w[MAX_N], v[MAX_N];
ll W;

pair<ll,ll> ps[1 << (MAX_N / 2)]; // (重量,价值)对
void solve()
{
//枚举前半部分
int n2 = n / 2;
for(int i=0;i<1<<n2;i++)
{
ll sw = 0, sv = 0;
for(int j=0;j<n2;j++)
{
if(i>>j & 1)
{
sw += w[j];
sv += v[j];
}
}
ps[i] = make_pair(sw,sv);
}
//去除多余元素
sort(ps, ps+(1<<n2));
int m = 1;
for(int i=1;i<1<<n2;i++)
{
if(ps[m-1].second < ps[i].second)
{
ps[m++] = ps[i];
}
}
//枚举后半部分并求解
ll res = 0;
for(int i=0;i<1<<(n-n2);i++)
{
ll sw = 0, sv = 0;
for(int j=0;j<n-n2;j++)
{
if(i>>j & 1)
{
sw += w[n2+j];
sv += v[n2+j];
}
}
if(sw <= W)
{
ll tv = (lower_bound(ps,ps+m,make_pair(W-sw,INF))-1)->second;
res = max(res, sv + tv);
}
}
printf("%lld\n", res);
}

3.2.5 坐标离散化

w*h的格子上画了n条垂直或水平的宽度为1的直线。求出这些线将格子划分成了多少个区域。
1<=w,h<=1000000. 1<=n<=500
样例:
w = 10, h = 10, n = 5
x1 = {1, 1, 4, 9, 10}
x2 = {6, 10, 4, 9, 10}
y1 = {4, 8, 1, 1, 6}
y2 ={4, 8, 10, 5, 10}

理解了半天这个样例,意思是( (x1, y1), (x2, y2) )是一条直线。一般求解被分割出的区域的个数使用图的遍历如DFS和BFS算法,需要$w\times h$,但是这个问题中w和h最大为1000000,所以没办法创建出$w\times h$的数组,所以需要使用坐标离散化技巧。

数组里只需要存储有直线的行列以及前后的行列就足够了,这样的话大小最多$6n \times 6n$就足够了(x1的自己、前后,3个,x2的自己、前后,3个,因此则有 $6n \times 6n$)。创建出数组并利用搜索求出区域的个数。(区域可能很大,所以用递归函数实现的话可能会栈溢出,所以在下列代码实现中不用DFS而用BFS)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
//输入
int W, H, N
int X1[MAX_N], X2[MAX_N], Y1[MAX_N], Y2[MAX_N];

bool fld[MAX_N*6][MAX_N*6];
int dx[4]={-1,0,0,1}, dy[4]={0,-1,1,0};

int compress(int *x1, int *x2; int w)
{
vector<int> xs;
for(int i=0;i<N;i++)
{
for(int d = -1;d<=1;d++)
{
int tx1 = x1[i] + d, tx2 = x2[i] + d;
if(1<=tx1 && tx1<=w) xs.push_back(tx1);
if(1<=tx2 && tx2<=w) xs.push_back(tx2);
}
}

sort(xs.begin(),xs,end());
xs.erase(unique(x.begin(),xs.end()),xs.end()); //去重
for(int i=0;i<N;i++) //重新建立x1, x2的值
{
x1[i] = find(xs.begin(),xs.end(),x1[i]) - xs.begin();
x2[i] = find(xs.begin(),xs.end(),x2[i]) - xs.begin();
}
return xs.size(); //因为有去重,所以不慌
}

void solve()
{
W = compress(X1,X2,W);
H = compress(Y1,Y2,H);

memset(fld, 0, sizeof(fld));
for(int i=0;i<N;i++)
{
for(int y=Y1[i];y<=Y2[i];y++)
{
for(int x=X1[i];x<=X2[i];x++)
{
fld[y][x] = true;
}
}
}

int ans = 0;
for(int y=0;y<H;y++)
{
for(int x=0;x<W;x++)
{
if(fld[y][x]) continue;
ans++;

queue<pair<int, int> > que;
que.push(make_pair(x,y));
while(!que.empty())
{
int sx = que.front().first(), sy = que.front().second();
que.pop();
for(int i=0;i<4;i++)
{
int tx = sx + dx[i], ty = sy + dy[i];
que.push(make_pair(tx,ty));
fld[ty][tx] = true;
}
}
}
}
printf("%d\n", ans);
}