例5【油漆面积】(线段树)
X星球的一批考古机器人正在一片废墟上考古。
该区域的地面坚硬如石、平整如镜。
管理人员为方便,建立了标准的直角坐标系。
每个机器人都各有特长、身怀绝技。它们感兴趣的内容也不相同。
经过各种测量,每个机器人都会报告一个或多个矩形区域,作为优先考古的区域。
矩形的表示格式为(x1,y1,x2,y2),代表矩形的两个对角点坐标。
为了醒目,总部要求对所有机器人选中的矩形区域涂黄色油漆。
小明并不需要当油漆工,只是他需要计算一下,一共要耗费多少油漆。
其实这也不难,只要算出所有矩形覆盖的区域一共有多大面积就可以了。
注意,各个矩形间可能重叠。
本题的输入为若干矩形,要求输出其覆盖的总面积。
输入格式:
第一行,一个整数n,表示有多少个矩形(1<=n<10000)
接下来的n行,每行有4个整数x1 y1 x2 y2,空格分开,表示矩形的两个对角顶点坐标。
(0<= x1,y1,x2,y2 <=10000)
输出格式:
一行一个整数,表示矩形覆盖的总面积。
例如,
输入:
3
1 5 10 10
3 1 20 20
2 7 15 17
程序应该输出:
340
再例如,
输入:
3
5 2 10 6
2 7 12 10
8 1 15 15
程序应该输出:
128
方法1:暴力法,假设整个范围在1000以内,可以在1000 x 1000 的范围每块测试在不在矩形中,sum累计(效率低)
方法2:x排序,切分,每个条形累计面积;y的有效长度转化成线段树问题
线段树问题:如图,将3-6进行拆分
class Comp implements Comparator {
public int compare(Object a,Object b){
int []aa = (int [])a;
int []bb = (int [])b;
return aa[2]-bb[2];
}
}
public class Main {
static int solve(int[][]data){
List list = new Vector();
for(int i=0;i<data.length;i++){
//取出坐标
int x1 = data[i][0];
int y1 = data[i][1];
int x2 = data[i][2];
int y2 = data[i][3];
if(x1==x2 || y1==y2) continue;
if(x1>x2){ int t=x1;x1=x2;x2=t;}//规范化
if(y1>y2){ int t=y1;y1=y2;y2=t;}
int[]a = new int[4];
a[0] = y1;
a[1] = y2;//y值跨度
a[2] = x1;
a[3] = 1;//起始边
int[]b = new int[4];
b[0] = y1;
b[1] = y2;
b[2] = x2;
b[3] = 2;//结束边
list.add(a);
list.add(b);
}
Collections.sort(list,new Comp());//对x轴方向进行排序,list中存着沿y轴的一条条边
return work(list);
}
static int work(List list){//核心方法 int[]0 1 2 3 ==>y1,y2,x,开始边1or结束边2
if(list.size()==0)
return 0;
//线段树是满二叉树,所有节点数可事先计算
final int N = 1024*16;//比10000大的最小的2次幂
int [][] tr = new int[N*2][2];//自身覆盖数,子孙覆盖总宽度
int []a = (int[])list.get(0);
int from = a[2];
add(tr,0,0,N,a[0],a[1]);
int sum = 0;
for(int i=0;i<list.size();i++){
int []it=(int[])list.get(i) ;
if(it[2]>from){
sum += tr[0][1] * (it[2]-from);
from = it[2];
}
if(it[3]==1)
add(tr,0,0,N,it[0],it[1]);
else
del(tr,0,0,N,it[0],it[1]);
}
return sum;
}
//tr:堆 idx:索引位置 a、b:跨度 y1、y2:
static void add(int[][]tr,int idx,int a,int b,int y1,int y2){//线段树操作
if(a==y1 && b==y2){//检验是否和当前结点匹配
tr[idx][0]++;//自身覆盖次数
if(tr[idx][0]==1)
cry(tr,(idx-1)/2,(b-a)-tr[idx][1]);//向父结点报告
return;
}
//必要时切割后递归
int h = (a+b)/2;
if(y2<=h){
add(tr,idx*2+1,a,h,y1,y2);
return;
}
if(y1>=h){
add(tr,idx*2+2,h,b,y1,y2);
return;
}
add(tr,idx*2+1,a,h,y1,h);
add(tr,idx*2+2,h,b,h,y2);
}
static void cry(int[][]tr,int idx,int delta){
if(delta==0) return;
tr[idx][1] +=delta;
if(tr[idx][0]==0 && idx>0)
cry(tr,(idx-1)/2,delta);
}
static void del(int[][]tr,int idx,int a,int b,int y1,int y2){
if(a==y1 && b==y2 && tr[idx][0]>0){
tr[idx][0]--;
if(tr[idx][0]==0)
cry(tr,(idx-1)/2,tr[idx][1]-(b-a));
return;
}
//必要时切割后递归
int h = (a+b)/2;
if(y2<=h){
del(tr,idx*2+1,a,h,y1,y2);
return;
}
if(y1>=h){
del(tr,idx*2+2,h,b,y1,y2);
return;
}
add(tr,idx*2+1,a,h,y1,h);
add(tr,idx*2+2,h,b,h,y2);
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = Integer.parseInt(sc.nextLine().trim());
int [][]data = new int[n][4];
for(int i=0;i<n;i++){
String []ss = sc.nextLine().trim().split(" +");
data[i] = new int[4];
for(int j=0;j<4;j++){
data[i][j] = Integer.parseInt(ss[j]);
}
}
System.out.println(solve(data));
}
}
例6【合根植物】(并查集)
w星球的一个种植园,被分成 m * n 个小格子(东西方向m行,南北方向n列)。每个格子里种了一株合根植物。
这种植物有个特点,它的根可能会沿着南北或东西方向伸展,从而与另一个格子的植物合成为一体。
如果我们告诉你哪些小格子间出现了连根现象,你能说出这个园中一共有多少株合根植物吗?
输入格式:
第一行,两个整数m,n,用空格分开,表示格子的行数、列数(1<m,n<1000)。
接下来一行,一个整数k,表示下面还有k行数据(0<k<100000)
接下来k行,第行两个整数a,b,表示编号为a的小格子和编号为b的小格子合根了。
格子的编号一行一行,从上到下,从左到右编号。
比如:5 * 4 的小格子,编号:
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
17 18 19 20
样例输入:
5 4
16
2 3
1 5
5 9
4 8
7 8
9 10
10 11
11 12
10 14
12 16
14 18
17 18
15 19
19 20
9 13
13 17
样例输出:
5
解析:一个图中,因为连通性分成多个集团,快速求两个节点间的连通性:dfs建生成树,从没访问的点中继续建生成树,将是否连通问题转化为生成树根结点是否相同。在查询的过程中,不断地改变父节点(并查集),使得纵深的树变成了扁平的树,,从而提高效率
public class Main {
static int []tb;
static int num = 0;
static void f(int a,int b){
int ra = find(a);
int rb = find(b);
if(ra==rb) return;
tb[ra]=rb;//树合并
num++;
}
static int find(int a){
if(tb[a]==0)
return a;
return tb[a] = find(tb[a]);//重新指定自己的祖先!!!变为扁平的树
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String [] ss = sc.nextLine().trim().split(" +");
int M = Integer.parseInt(ss[0]);
int N = Integer.parseInt(ss[1]);
tb = new int [M*N+1];//标号从1开始
int k =Integer.parseInt((sc.nextLine().trim()));
for(int i=0;i<k;i++){
ss = sc.nextLine().trim().split(" +");
f(Integer.parseInt(ss[0]),Integer.parseInt(ss[1]));
}
System.out.println(M*N-num);
}
}
例7【青蛙跳杯子】
X星球的流行宠物是青蛙,一般有两种颜色:白色和黑色。
X星球的居民喜欢把它们放在一排茶杯里,这样可以观察它们跳来跳去。
如下图,有一排杯子,左边的一个是空着的,右边的杯子,每个里边有一只青蛙。
*WWWBBB
其中,W字母表示白色青蛙,B表示黑色青蛙,*表示空杯子。
X星的青蛙很有些癖好,它们只做3个动作之一:
1. 跳到相邻的空杯子里。
2. 隔着1只其它的青蛙(随便什么颜色)跳到空杯子里。
3. 隔着2只其它的青蛙(随便什么颜色)跳到空杯子里。
对于上图的局面,只要1步,就可跳成下图局面:
WWW*BBB
本题的任务就是已知初始局面,询问至少需要几步,才能跳成另一个目标局面。
输入为2行,2个串,表示初始局面和目标局面。
输出要求为一个整数,表示至少需要多少步的青蛙跳。
例如:
输入:
*WWBB
WWBB*
则程序应该输出:
2
再例如,
输入:
WWW*BBB
BBB*WWW
则程序应该输出:
10
我们约定,输入的串的长度不超过15
----------------------------
笨笨有话说:
我梦见自己是一棵大树,
青蛙跳跃,
我就发出新的枝条,
春风拂动那第 5 层的新枝,
哦,我已是枝繁叶茂。
解析:与分酒问题类似
public class Main {
static Set jump(String s){
Set<String> set = new HashSet();
char [] c = s.toCharArray();
for(int i=0;i<c.length;i++)
if(c[i]=='*')
for(int j=-3;j<4;j++){
if(j==0) continue;
if(i+j>=0 && i+j<c.length && c[i+j]!='*'){
try{
c[i] = c[i+j];c[i+j] = '*';set.add(new String(c));//试探
}
finally{
c[i+j] = c[i];c[i] = '*';//回溯
}
}
}
return set;
}
static int f(Set<String> begin,Set<String> his,String end){
if(begin.contains(end))
return 0;
Set<String> ns = new HashSet();
for(String s:begin){
ns.addAll(jump(s));
}
ns.removeAll(his);
if(ns.isEmpty()) return -1;
his.addAll(ns);
int r = f(ns,his,end);
if(r<0) return -1;
return r+1;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String begin = sc.nextLine().trim();
String end = sc.nextLine().trim();
Set<String> set = new HashSet();
Set<String> his = new HashSet();
set.add(begin);
his.addAll(set);
System.out.println(f(set,his,end));
}
}