
黑白棋・改:无限大的棋盘上,在横向和纵向上被包围的白子会变成黑子,求最终黑子个数?
3.3活用各种数据结构
Binary Indexed Tree
首先需要了解一下扫描线算法,扫描线算法是计算几何学上对多边形染色的算法。

如图,扫描线算法的目标是计算黄颜色多边形内部的像素点,最终将其涂色。怎么知道哪些点在内部呢?首先从顶点中y轴最小坐标出发,到最大坐标为止,画一条条水平的扫描线,扫描线必然与多边形有多个交点。第一个与第二个,第三个与第四个……之间的点就是内部点。
用严密的语言描述如下:
基本思想
按扫描线顺序,计算扫描线与多边形的相交区间,再用要求的颜色显示这些区间的象素,即完成填充工作。
对于一条扫描线填充过程可以分为四个步骤:
(1) 求交:计算扫描线与多边形各边的交点
(2) 排序:把所有交点按 x 坐标递增顺序来排序
(3) 配对:确定扫描线与多边形的相交区间,第一个与第二个,第三个与第四个等等,每对交点代表扫描线与多边形的一个相交区间
(4) 填充:显示相交区间的象素
回到题目,题目给的是一张无限大的棋盘,所以需要先进行坐标压缩,压缩之后采取扫描线法计算内部点数即可。

上图红色方格即为要染色的点,计算两个交点之间的空白点的数量的时候,由于它们的Y坐标肯定相等,那么问题转化为统计两个交点x坐标之间的点的个数。涉及到累积和,轮到树状数组出场了。
统计x的时候,需要将一个区间[交点n的x + 1,交点n+1的x – 1]的个数统统加一,树状数组本身做加法的话是logn,做n次的话是nlogn,太浪费了,BIT可以高效地求一段区间和,以及更新单个元素的值,但是无法高效地更新一个区间的值。
于是可以用双BIT来实现,一个维护初始个数,一个维护累加值,可以将复杂度控制在logn。
具体说来,可以用如下方式定义这两个BIT:bit0和bit1

其中bit1表示增量的累计和,如图:


对区间[l,r]增加x之后,前缀和的增量如上图所示。我们用bit1来维护每个元素的增加量,当然,执行add(i,x)后并不是每个元素都会改变,所以还需要用sum(bit0,i)来维护初始值的修正值。准确来说,只有l和r之间的元素才会+x,所以表示增加量的bit1[l] += x,bit1[r + 1] += -x(这里的思想和imos法是一致的,记录变化量,然后累加),假设i<l,这次add不影响sum。假设i位于l和r之间,sum(bit1,i)*i = xi,xi的意思是前i个元素每个都+x,看起来比正确答案多加了(1-l)*x的样子,所以修正值bit0必须执行bit0[l]=-(l-1)*x。假设i>r,表示增加量的sum(bit1,i)根本没发生变化(因为l位置的x和r+1位置的-x抵消掉了),这时候就得依靠bit0在r+1位置的xr减去l位置的x(l-1)得出正确答案x(r-l+1)作为最终的结果了。
这样子就可以理解上面的4个操作的由来了。嘛,尽量理解吧。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
#define MAX_N 100000 + 16
typedef long long LL;
LL bit0[MAX_N], bit1[MAX_N]; // 双BIT实现O(logn)区间加法
int N, X[MAX_N], Y[MAX_N];
vector<int> line[MAX_N]; // 每个黑子构成的一条条水平线,y坐标作为下标
bool visited[MAX_N]; // 是否已经算过了
// 求和sum{a[t], 0 <= t <= i}
LL sum(LL *b, int i)
{
int s = 0;
while (i > 0)
{
s += b[i];
i -= (i & -i);
}
return s;
}
// 求和a[from, to)
LL sum(LL* bit, int from, int to)
{
return sum(bit, to - 1) - sum(bit, from - 1);
}
// 执行 a[i] += v
void add(LL *b, int i, LL v)
{
while (i <= MAX_N)
{
b[i] += v;
i += (i & -i);
}
}
// 基础bit结束
// 双BIT
//[0,n]
LL sum(int i)
{
return sum(bit1, i) * i + sum(bit0, i);
}
//(from, to]
LL sum(int from, int to)
{
return sum(to) - sum(from);
}
// [from, to]
void add(int from, int to, LL x)
{
add(bit0, from, -x * (from - 1));
add(bit1, from, x);
add(bit0, to, x * to);
add(bit1, to, -x);
}
// 压缩坐标,将坐标的值变成“这是第几种值”,返回一共有几种坐标
// x为坐标数组,长度为N
int compress(int *p)
{
vector<int> ps(N);
for (int i = 0; i < N; ++i)
{
ps[i] = p[i];
}
sort(ps.begin(), ps.end());
ps.erase(unique(ps.begin(), ps.end()), ps.end());
for (int i = 0; i < N; ++i)
{
p[i] = 1 + distance(ps.begin(), lower_bound(ps.begin(), ps.end(), p[i])); // 此处加1为了防止 i += (i & -i); 永远为0
}
return ps.size();
}
///////////////////////////SubMain//////////////////////////////////
int main(int argc, char *argv[])
{
#ifndef ONLINE_JUDGE
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
#endif
scanf("%d", &N);
for (int i = 0; i < N; ++i)
{
scanf("%d%d", X + i, Y + i);
}
int w = compress(X);
int h = compress(Y);
for (int i = 0; i < N; ++i)
{
line[Y[i]].push_back(X[i]); // 一条条扫描线
}
LL result = N;
for (int y = 1; y <= h; ++y)
{
// 对每条扫描线,计算上面空白点的个数
vector<int>& xs = line[y]; // 纵坐标为y的那些点的横坐标,一条扫描线
sort(xs.begin(), xs.end());
for (vector<int>::iterator i = xs.begin(); i != xs.end(); ++i)
{
int x = *i;
LL s = sum(x - 1, x); // 横坐标为x的空白点个数
if (visited[x])
{
result += s; // 就是要染色的像素点
}
else
{
visited[x] = true;
}
add(bit0, x, -s); // 等同于add(x, x, -s);即对count[x] -= s;这是一个复位操作,为下次统计做准备
if (i + 1 != xs.end())
{
// 到下一个黑点之间空白点的个数
// 这两个分支合并起来就是 add(x + 1, next_x - 1, 1);的意思
if (x + 1 < *(i + 1) - 1) // 下一个黑子
{
add(x + 1, *(i + 1) - 1, 1); // 遇到了空白
}
else if (x + 1 == *(i + 1) - 1)
{
add(bit0, x + 1, 1); // 遇到了空白,此时等同于add(x + 1, x + 1, 1);
}
}
}
}
cout << result << endl;
#ifndef ONLINE_JUDGE
fclose(stdin);
fclose(stdout);
system("out.txt");
#endif
return 0;
}
///////////////////////////End Sub//////////////////////////////////
| 12956715 | hankcs | 3109 | Accepted | 7328K | 3469MS | C++ | 2599B | 2014-06-07 23:22:36 |
知识共享署名-非商业性使用-相同方式共享:码农场 » POJ 3109 Inner Vertices 题解 《挑战程序设计竞赛》
码农场