WHUT第十一週訓練整理
寫在前面的話:我的能力也有限,錯誤是在所難免的!因此如發現錯誤還請指出一同學習!
索引
(難度由題目自身難度與本週做題情況進行分類,僅供新生參考!)
零、基礎知識過關
一、easy:02、04、05、07、10、11、12、18
二、medium:03、06、08、09、16、17
三、hard:01、13、14、15、19
零、基礎知識過關
終於來了一場合胃口的,之前兩場搞得我心態有點爆炸!不過這周的題目有些還是比較難的,量力而爲吧!
首先線段樹在ACM中是非常重要的數據結構,在很多的題目中都會涉及到,與其去學一堆冷門的算法,打比賽的時候也沒有機會讓你開那些題,還不如把線段樹學好了!
我的博客中也有挺多線段樹相關的,這裏給個鏈接:
一、easy
1002:敵兵佈陣(線段樹)
wwwww
題意:有 個工兵營地,營地人數爲 , 有若干條命令,共 種形式:(1),表示 營地增加 個人;(2) ,表示 營地減少 個人;(3),查詢營地 到營地 中的總人數;(4) ,命令結束。
範圍:
分析:線段樹板子題,根據給的操作維護一個區間和,進行單點修改以及區間查詢即可,常規操作。
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 5e4 + 10;
// ans 用來保存查詢的結果
int n, ans;
struct Node
{
int l, r;
int sum;
} tree[4 * MAXN];
void pushUp(int k)
{
tree[k].sum = tree[2 * k].sum + tree[2 * k + 1].sum;
}
void build(int k, int l, int r)
{
tree[k].l = l, tree[k].r = r;
if (l == r)
{
cin >> tree[k].sum;
return;
}
int mid = l + r >> 1;
build(2 * k, l, mid);
build(2 * k + 1, mid + 1, r);
pushUp(k);
}
// 查詢區間和
void ask_interval(int k, int l, int r)
{
if (tree[k].l >= l && tree[k].r <= r)
{
ans += tree[k].sum;
return;
}
int mid = tree[k].l + tree[k].r >> 1;
if (l <= mid)
ask_interval(2 * k, l, r);
if (mid < r)
ask_interval(2 * k + 1, l, r);
}
// 單點修改
void change_point(int k, int x, int c)
{
if (tree[k].l == tree[k].r)
{
tree[k].sum += c;
return;
}
int mid = tree[k].l + tree[k].r >> 1;
if (x <= mid)
change_point(2 * k, x, c);
else
change_point(2 * k + 1, x, c);
pushUp(k);
}
int main()
{
// C++關閉流同步,否則會TLE,或者用C也行
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int T;
cin >> T;
int kase = 1;
while (T--)
{
cin >> n;
build(1, 1, n);
cout << "Case " << kase++ << ":" << endl;
string str;
while (cin >> str, str != "End")
{
int a, b;
cin >> a >> b;
if (str == "Query")
{
// 注意查詢前將ans清空
ans = 0;
ask_interval(1, a, b);
cout << ans << endl;
}
else if (str == "Add")
{
change_point(1, a, b);
}
else
{
change_point(1, a, -b);
}
}
}
return 0;
}
1004:Counting Squares(線段樹+掃描線)
題意:給若干個矩形的兩個對角點 ,求整個圖形的面積。
範圍:
分析:能做出 1003 的這道題肯定沒有問題,這道題目甚至都不需要離散化,直接對區間 進行建樹跑掃描線即可。
Notice:題目說給的是兩個對角點!沒說左下+右上還是左上+右下,所以需要我們手動判斷一下。
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e4 + 10;
struct Line
{
int x1, x2, y;
int flag; // flag用來標記是出邊還是入邊
bool operator<(Line other) const
{
return y < other.y;
}
} line[MAXN * 2];
struct Node
{
int l, r;
int cnt, len; // cnt表示被覆蓋的次數,len表示當前矩形底邊長之和
} tree[4 * MAXN];
int n;
void build(int k, int l, int r)
{
tree[k].l = l, tree[k].r = r;
tree[k].cnt = tree[k].len = 0;
if (l == r)
{
return;
}
int mid = tree[k].l + tree[k].r >> 1;
build(2 * k, l, mid);
build(2 * k + 1, mid + 1, r);
}
void pushUp(int k)
{
if (tree[k].cnt)
tree[k].len = tree[k].r - tree[k].l + 1;
else if (tree[k].l == tree[k].r)
tree[k].len = 0;
else
tree[k].len = tree[2 * k].len + tree[2 * k + 1].len;
}
void change_interval(int k, int l, int r, int c)
{
if (tree[k].l >= l && tree[k].r <= r)
{
tree[k].cnt += c;
pushUp(k); // 注意這裏也需要push一下
return;
}
int mid = tree[k].l + tree[k].r >> 1;
if (l <= mid)
change_interval(2 * k, l, r, c);
if (mid < r)
change_interval(2 * k + 1, l, r, c);
pushUp(k);
}
int main()
{
int a, b, c, d;
int ans = 0;
int index = 0;
while (cin >> a >> b >> c >> d)
{
if (a == b && b == c && c == d && (d == -1 || d == -2))
{
sort(line, line + index);
build(1, 0, MAXN);
for (int i = 0; i < index - 1; i++)
{
change_interval(1, line[i].x1, line[i].x2 - 1, line[i].flag);
// 底邊長之和*高度
ans += (line[i + 1].y - line[i].y) * tree[1].len;
}
cout << ans << endl;
index = ans = 0;
if (d == -2)
break;
continue;
}
// 給的是對角點,需要自己判斷一下
if (a > c)
swap(a, c);
if (b > d)
swap(b, d);
line[index++] = {a, c, b, 1};
line[index++] = {a, c, d, -1};
}
return 0;
}
1005:Minimum Inversion Number(逆序對+預處理)
題意:給一個 的排列,可以循環移動讓任意一個數字打頭,問最小的逆序數是多少?
範圍:
分析:數據範圍不大,不需要使用線段樹/樹狀數組等數據結構。
直接先雙重循環求出原始序列的逆序對,預處理出數組 和 ,分別表示數字 左右兩邊比 大的數字的個數。
每當一個數字移動到最後時,原先右側的數字與自身的逆序關係發生了翻轉,那麼逆序數就改變了 ;
而左側的數字在前面的操作中已經移動到右側,當前數字移動到右側之後與這些數字的逆序關係恢復,改變了 ;
那麼把上面的答案合併一下,改變量 。
於是模擬一下移動的過程更新答案即可。
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 5000 + 10;
// l:左邊比當前大的數數量 r:右邊比當前數大的數數量
int arr[MAXN], l[MAXN], r[MAXN];
int main()
{
int n;
while (cin >> n)
{
// 清空數組
memset(l, 0, sizeof(l));
memset(r, 0, sizeof(r));
for (int i = 0; i < n; i++)
{
cin >> arr[i];
}
int ans = 0;
// 處理出兩個數組,同時計算出原始的逆序數
for (int i = 0; i < n; i++)
{
for (int j = i + 1; j < n; j++)
{
if (arr[j] > arr[i])
{
r[i]++;
}
}
for (int j = 0; j < i; j++)
{
if (arr[j] > arr[i])
{
l[i]++;
}
}
ans += l[i];
}
// temp用來保存模擬循環移動的中間值
int temp = ans;
for (int i = 0; i < n - 1; i++)
{
temp += 2 * (l[i] + r[i]) - n + 1;
// temp += 2 * r[i] - n + i + 1 + 2 * l[i] - i;
// cout << "temp: " << temp << endl;
ans = min(ans, temp);
}
cout << ans << endl;
}
return 0;
}
1007:Stars(線段樹)
題意:有天空中 顆星星的位置 ,定義星星的等級爲其左下方星星的數量(不包括自己),問每種等級的星星各有多少。
範圍:
分析:我們需要統計的是每個星星左下角的星星數量,我們可以先將所有的星星按照座標 軸進行排序,那麼接下來掃描每個點,我們要求的就是之前高度小於當前點的星星數量,那麼問題就好辦了。因爲 座標的值域不大,所以不需要離散化,直接使用線段樹對 座標進行建樹,每次掃描到一個點,把這個點的 座標加入樹中,那麼對所有點我們就只需要對線段樹進行一次區間查詢即可。
詳見代碼。
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 32000 + 10;
int n, ans;
// num 保存每種等級星星的數量
int num[MAXN];
struct Node
{
int l, r;
int sum;
} tree[4 * MAXN];
struct Point
{
int x, y;
bool operator<(Point other) const
{
if (x != other.x)
return x < other.x;
else
return y < other.y;
}
} p[MAXN];
void pushUp(int k)
{
tree[k].sum = tree[2 * k].sum + tree[2 * k + 1].sum;
}
void build(int k, int l, int r)
{
tree[k].l = l, tree[k].r = r;
tree[k].sum = 0;
if (l == r)
return;
int mid = l + r >> 1;
build(2 * k, l, mid);
build(2 * k + 1, mid + 1, r);
}
void ask_interval(int k, int l, int r)
{
if (tree[k].l >= l && tree[k].r <= r)
{
ans += tree[k].sum;
return;
}
int mid = tree[k].l + tree[k].r >> 1;
if (l <= mid)
ask_interval(2 * k, l, r);
if (mid < r)
ask_interval(2 * k + 1, l, r);
}
void change_point(int k, int x, int c)
{
if (tree[k].l == tree[k].r)
{
tree[k].sum += c;
return;
}
int mid = tree[k].l + tree[k].r >> 1;
if (x <= mid)
change_point(2 * k, x, c);
else
change_point(2 * k + 1, x, c);
pushUp(k);
}
int main()
{
while (cin >> n)
{
memset(num, 0, sizeof(num));
build(1, 0, MAXN);
for (int i = 0; i < n; i++)
{
cin >> p[i].x >> p[i].y;
}
sort(p, p + n);
for (int i = 0; i < n; i++)
{
// 查詢前面高度低於當前點的點數量
ans = 0;
ask_interval(1, 0, p[i].y);
num[ans]++;
// 將當前點加入線段樹
change_point(1, p[i].y, 1);
}
for (int i = 0; i < n; i++)
{
cout << num[i] << endl;
}
}
return 0;
}
1010:Color the ball(線段樹)
題意:有 個編號爲 的氣球,同時進行 次操作,每次給編號在 中的連續氣球進行統一塗色,全部操作結束之後問每個氣球被塗色的總次數。
範圍:
分析:線段樹板子題,區間修改,單點查詢。
詳見代碼。
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e5 + 10;
int n, ans;
struct Node
{
int l, r;
int sum, f;
} tree[4 * MAXN];
void build(int k, int l, int r)
{
tree[k].l = l, tree[k].r = r;
tree[k].sum = tree[k].f = 0;
if (l == r)
{
return;
}
int mid = l + r >> 1;
build(2 * k, l, mid);
build(2 * k + 1, mid + 1, r);
}
void pushUp(int k)
{
tree[k].sum = tree[2 * k].sum + tree[2 * k + 1].sum;
}
void down(int k)
{
tree[2 * k].sum += tree[k].f * (tree[2 * k].r - tree[2 * k].l + 1);
tree[2 * k].f += tree[k].f;
tree[2 * k + 1].sum += tree[k].f * (tree[2 * k + 1].r - tree[2 * k + 1].l + 1);
tree[2 * k + 1].f += tree[k].f;
tree[k].f = 0;
}
void change_interval(int k, int l, int r, int c)
{
if (tree[k].l >= l && tree[k].r <= r)
{
tree[k].sum += c * (tree[k].r - tree[k].l + 1);
tree[k].f += c;
return;
}
if (tree[k].f)
down(k);
int mid = tree[k].l + tree[k].r >> 1;
if (l <= mid)
change_interval(2 * k, l, r, c);
if (mid < r)
change_interval(2 * k + 1, l, r, c);
pushUp(k);
}
void ask_point(int k, int x)
{
if (tree[k].l == tree[k].r)
{
ans = tree[k].sum;
return;
}
if (tree[k].f)
down(k);
int mid = tree[k].l + tree[k].r >> 1;
if (x <= mid)
ask_point(2 * k, x);
else
ask_point(2 * k + 1, x);
}
int main()
{
while (cin >> n, n)
{
build(1, 1, n);
for (int i = 0; i < n; i++)
{
int l, r;
cin >> l >> r;
change_interval(1, l, r, 1);
}
for (int i = 1; i <= n; i++)
{
if (i > 1)
cout << " ";
ans = 0;
ask_point(1, i);
cout << ans;
}
cout << endl;
}
return 0;
}
1011:Just a Hook(線段樹)
題意:有一根長度爲 的鉤子,進行 次操作,每次將 這一段鉤子的材質變成 ,問全部操作結束後鉤子的總材質值。
範圍:
分析:線段樹板子題,只需要區間修改即可。
詳見代碼。
Code:
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn = 1e5 + 5;
long long ans;
struct Node
{
int left, right;
long long w;
long long f;
} tree[4 * maxn];
void build(int k, int l, int r)
{
tree[k].left = l;
tree[k].right = r;
tree[k].f = 0;
if (l == r)
{
tree[k].w = 1;
return;
}
int m = (l + r) / 2;
build(2 * k, l, m);
build(2 * k + 1, m + 1, r);
tree[k].w = tree[2 * k].w + tree[2 * k + 1].w;
}
void down(int k)
{
tree[2 * k].f = tree[k].f;
tree[2 * k + 1].f = tree[k].f;
tree[2 * k].w = tree[k].f * (tree[2 * k].right - tree[2 * k].left + 1);
tree[2 * k + 1].w = tree[k].f * (tree[2 * k + 1].right - tree[2 * k + 1].left + 1);
tree[k].f = 0;
}
void change_interval(int k, long long a, long long b, long long c)
{
if (tree[k].left >= a && tree[k].right <= b)
{
tree[k].w = c * (tree[k].right - tree[k].left + 1);
tree[k].f = c;
return;
}
if (tree[k].f)
down(k);
int m = (tree[k].left + tree[k].right) / 2;
if (a <= m)
change_interval(2 * k, a, b, c);
if (b > m)
change_interval(2 * k + 1, a, b, c);
tree[k].w = tree[2 * k].w + tree[2 * k + 1].w;
}
int main()
{
int T;
scanf("%d", &T);
int kase = 0;
while (T--)
{
int N, Q;
scanf("%d%d", &N, &Q);
build(1, 1, N);
for (int i = 0; i < Q; i++)
{
// 數值比較大,注意開longlong
long long a, b, c;
scanf("%lld%lld%lld", &a, &b, &c);
change_interval(1, a, b, c);
}
// 根節點的權值就是整個鉤子的權值
ans = tree[1].w;
printf("Case %d: The total value of the hook is %lld.\n", ++kase, ans);
}
return 0;
}
1012:I Hate It(線段樹)
題意:有 個同學(編號爲 )的成績,且有 個操作,操作共兩種:(1),表示查詢編號區間爲 中的成績最高值;(2),表示將同學 的成績改成 。
範圍:
分析:線段樹板子題,區間查詢,單點修改,維護一個最大值。
詳見代碼。
Code:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 2e5;
int ans;
struct Node
{
int left, right;
int w;
int f;
} tree[4 * maxn + 1];
void build(int l, int r, int k)
{
tree[k].left = l;
tree[k].right = r;
if (l == r)
{
scanf("%d", &tree[k].w);
return;
}
int m = (l + r) / 2;
build(l, m, k * 2);
build(m + 1, r, k * 2 + 1);
tree[k].w = max(tree[2 * k].w, tree[2 * k + 1].w);
}
void down(int k)
{
tree[k * 2].f += tree[k].f;
tree[k * 2 + 1].f += tree[k].f;
tree[k * 2].w += tree[k].f * (tree[k * 2].right - tree[k * 2 + 1].left + 1);
tree[k * 2 + 1].w += tree[k].f * (tree[k * 2 + 1].right - tree[k * 2 + 1].left + 1);
tree[k].f = 0;
}
void change_point(int k, int x, int y)
{
if (tree[k].left == tree[k].right)
{
tree[k].w = y;
return;
}
// if(tree[k].f) down(k);
int m = (tree[k].left + tree[k].right) / 2;
if (x <= m)
change_point(2 * k, x, y);
else
change_point(2 * k + 1, x, y);
tree[k].w = max(tree[2 * k].w, tree[2 * k + 1].w);
}
void ask_interval(int k, int a, int b)
{
if (tree[k].left >= a && tree[k].right <= b)
{
ans = max(ans, tree[k].w);
return;
}
// if(tree[k].f) down(k);
int mid = (tree[k].left + tree[k].right) / 2;
if (a <= mid)
ask_interval(2 * k, a, b);
if (b > mid)
ask_interval(2 * k + 1, a, b);
}
int main()
{
int n, m;
while (scanf("%d%d", &n, &m) == 2)
{
build(1, n, 1);
for (int i = 0; i < m; i++)
{
getchar();
char ch;
scanf("%c", &ch);
if (ch == 'U')
{
int x, y;
scanf("%d%d", &x, &y);
change_point(1, x, y);
}
else
{
int a, b;
scanf("%d%d", &a, &b);
ans = 0;
ask_interval(1, a, b);
printf("%d\n", ans);
}
}
}
return 0;
}
1018:Light(線段樹+貪心)
題意:有 個開着或關着的燈籠排成一排,現在有一種開關可以選擇從任意位置開始控制連續 個燈籠的開關狀態,讓這 個燈籠的狀態全部反轉,現在要至少需要多少個這樣的開關才能讓所有燈籠全亮,沒有可行解則輸出 。
範圍:
分析:我們需要讓所有的燈亮起來,那麼我們從左往右考慮的話,如果當前的燈是暗的,那麼我們就需要在這個位置安裝一個開關來將這個燈點亮看,這個時候我們已經假設前面的燈已經通過前面的開關全部點亮了,此時需要改變當前燈的狀態只能重新安裝一個開關,因此就用這樣的想法使用線段樹來進行區間修改模擬,最後再對每個點進行單點查詢檢查是否全部點亮即可。
詳見代碼。
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e5 + 10;
int n, m, ans;
int arr[MAXN];
struct Node
{
int l, r;
int light, f;
} tree[4 * MAXN];
void down(int k)
{
// 葉子節點需要特判
if (tree[2 * k].l == tree[2 * k].r)
tree[2 * k].light = !tree[2 * k].light;
if (tree[2 * k + 1].l == tree[2 * k + 1].r)
tree[2 * k + 1].light = !tree[2 * k + 1].light;
// 狀態翻轉
tree[2 * k].f = !tree[2 * k].f;
tree[2 * k + 1].f = !tree[2 * k + 1].f;
tree[k].f = 0;
}
void build(int k, int l, int r)
{
tree[k].l = l, tree[k].r = r;
tree[k].f = 0;
if (l == r)
{
tree[k].light = arr[l];
return;
}
int mid = l + r >> 1;
build(2 * k, l, mid);
build(2 * k + 1, mid + 1, r);
}
void ask_point(int k, int x)
{
if (tree[k].l == tree[k].r)
{
ans = tree[k].light;
return;
}
if (tree[k].f)
down(k);
int mid = tree[k].l + tree[k].r >> 1;
if (x <= mid)
ask_point(2 * k, x);
else
ask_point(2 * k + 1, x);
}
void change_interval(int k, int l, int r)
{
if (tree[k].l >= l && tree[k].r <= r)
{
// 葉子節點需要判斷
if (tree[k].l == tree[k].r)
tree[k].light = !tree[k].light;
// 狀態翻轉
tree[k].f = !tree[k].f;
return;
}
if (tree[k].f)
down(k);
int mid = tree[k].l + tree[k].r >> 1;
if (l <= mid)
change_interval(2 * k, l, r);
if (mid < r)
change_interval(2 * k + 1, l, r);
}
// 調試函數
void show()
{
cout << "-------------------" << endl;
for (int i = 1; i <= 7; i++)
{
cout << "[" << tree[i].l << "," << tree[i].r << "] " << tree[i].light << " " << tree[i].f << endl;
}
}
int main()
{
while (cin >> n >> m, n + m)
{
for (int i = 1; i <= n; i++)
{
char ch;
cin >> ch;
arr[i] = ch - '0';
}
build(1, 1, n);
int cnt = 0;
for (int i = 1; i + m - 1 <= n; i++)
{
ans = 0;
ask_point(1, i);
// 如果是0的話必須安裝開關,區間修改
if (ans == 0)
{
cnt++;
change_interval(1, i, i + m - 1);
// show();
}
}
// 檢查是否每個點都被點亮
for (int i = 1; i <= n; i++)
{
ans = 0;
ask_point(1, i);
if (ans == 0)
{
cnt = -1;
break;
}
}
cout << cnt << endl;
}
return 0;
}
二、medium
1003:覆蓋的面積(線段樹+掃描線)
題意:給 個矩形的左下與右上頂點座標 ,矩形的邊與座標軸平行,求出被這些矩形覆蓋過至少兩次的區域的面積。
範圍:
分析:這道題是掃描線的進階,不知道掃描線的可以參考我之前寫的博客:線段樹之掃描線
這道題目需要輸出的是重疊部分的面積,那要怎麼操作呢?
線段樹結點我們需要保存的是覆蓋一次的長度 、覆蓋多次的長度 以及該區間被完全覆蓋的次數 。
維護覆蓋一次的長度就是掃描線的基本操作,不再贅述。
主要說說覆蓋多次的長度,分類談論:
① 如果當前區間被完全覆蓋的次數 ,那麼整個區間都是滿足條件的,此時 。
② 如果當前區間被完全覆蓋的次數爲 ,那麼只需要把兩個子區間覆蓋一次的長度相加就可以了,爲什麼呢?因爲本題我們不需要進行下傳的操作,所以上層區間被覆蓋了不會影響到下層的區間,因此在計算當前層的時候,雖然兩個子區間的範圍合起來跟自身是一樣的,但是他們被覆蓋的長度是獨立開來的,如果當前層完全覆蓋了一層,那麼兩個子區間覆蓋一次的長度實際上就是覆蓋了兩次的長度!此時 。
③ 如果當前區間沒有被完全覆蓋,那麼只能寄希望於兩個子區間了,此時 。
其餘的部分就是跟普通的掃描線一樣了,詳見代碼。
Notice:這題實在是太毒了,在控制精度的時候還是用 和 吧,這題用 和 會瘋狂WA的,原因是在某些環境下某些數值比如 的實際存儲值不一樣使用 C++ 的 可能會有風險,以後需要控制精度的還是用 吧!
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e5 + 10;
struct Line
{
double x1, x2, y;
int flag;
bool operator<(Line other) const
{
return y < other.y;
}
} line[MAXN * 2];
struct Node
{
int l, r;
int cnt;
double len1, len2; // len1表示僅被覆蓋一次的長度,len2表示被覆蓋多次的長度
} tree[4 * MAXN];
int n;
double lisan[MAXN * 2]; // 離散數組
void build(int k, int l, int r)
{
tree[k].l = l, tree[k].r = r;
tree[k].cnt = tree[k].len1 = tree[k].len2 = 0;
if (l == r)
{
return;
}
int mid = tree[k].l + tree[k].r >> 1;
build(2 * k, l, mid);
build(2 * k + 1, mid + 1, r);
}
void pushUp(int k)
{
// 如果該區間已經被完全覆蓋,那整個區間的長度即所求
if (tree[k].cnt)
{
tree[k].len1 = lisan[tree[k].r + 1] - lisan[tree[k].l];
}
// 如果是葉子節點則進行特判
else if (tree[k].l == tree[k].r)
{
tree[k].len1 = 0;
}
// 否則只能寄希望於兩個子區間了
else
{
tree[k].len1 = tree[2 * k].len1 + tree[2 * k + 1].len1;
}
// 如果該區間已經被完全覆蓋了多次,那麼整個區間的長度即所求
if (tree[k].cnt > 1)
{
tree[k].len2 = lisan[tree[k].r + 1] - lisan[tree[k].l];
}
// 特判葉子節點
else if (tree[k].r == tree[k].l)
{
tree[k].len2 = 0;
}
// 如果被完全覆蓋了一次,那麼答案就是子區間的單次覆蓋長度之和
else if (tree[k].cnt == 1)
{
tree[k].len2 = tree[2 * k].len1 + tree[2 * k + 1].len1;
}
// 否則只能寄希望於兩個子區間了
else
{
tree[k].len2 = tree[2 * k].len2 + tree[2 * k + 1].len2;
}
}
void change_interval(int k, int l, int r, int c)
{
if (tree[k].l >= l && tree[k].r <= r)
{
tree[k].cnt += c;
pushUp(k); // 注意push
return;
}
int mid = tree[k].l + tree[k].r >> 1;
if (l <= mid)
change_interval(2 * k, l, r, c);
if (mid < r)
change_interval(2 * k + 1, l, r, c);
pushUp(k);
}
int main()
{
int T;
scanf("%d", &T);
while (T--)
{
scanf("%d", &n);
int index = 0, len = 0;
// 對x進行離散化,保存每條線
for (int i = 1; i <= n; i++)
{
double x1, y1, x2, y2;
scanf("%lf%lf%lf%lf", &x1, &y1, &x2, &y2);
lisan[len++] = x1;
lisan[len++] = x2;
line[index++] = {x1, x2, y1, 1};
line[index++] = {x1, x2, y2, -1};
}
sort(line, line + index);
sort(lisan, lisan + len);
len = unique(lisan, lisan + len) - lisan;
build(1, 0, len - 1);
double ans = 0;
for (int i = 0; i < index - 1; i++)
{
int l = lower_bound(lisan, lisan + len, line[i].x1) - lisan;
int r = lower_bound(lisan, lisan + len, line[i].x2) - lisan - 1;
// cout << "l: " << l << " r: " << r << endl;
change_interval(1, l, r, line[i].flag);
// cout << tree[1].len2 << endl;
// cout << tree[1].len1 << endl;
ans += (line[i + 1].y - line[i].y) * tree[1].len2;
}
// 這裏千萬不要用cout!!!
printf("%.2f\n", ans);
}
return 0;
}
1006:Tunnel Warfare(線段樹+區間合併)
題意:有 個排成一排連通的村莊以及 個事件,有三種不同的事件:(1),摧毀第 個村莊;(2),查詢第 個村莊直接與間接連接的村莊數量;(3),恢復上一次被摧毀的村莊
範圍:
分析:經典的線段樹區間合併應用,如果還沒有學過的同學可以看看我的博客,介紹了一下基本的區間合併:線段樹之區間合併
知道怎麼利用區間合併求得某個村莊 直接和間接連接的村莊數量後,其他的問題就比較好解決了。(1)就是簡單的線段樹單點修改,(3)則可以利用棧來保存每次摧毀的村莊,調用(3)時則出棧,進行線段樹單點修改。
詳見代碼。
Code:
#include <iostream>
#include <cstdio>
#include <stack>
using namespace std;
const int maxn = 5e4 + 10, maxm = 5e4 + 10;
int n, m, ans;
stack<int> pre; // 倒敘保存被摧毀的村莊
struct Node
{
int left, right;
int cntLeft, cntRight; // 左邊連續的長度以及右邊連續的長度
int len; // 區間的總長度
} tree[4 * maxn];
void build(int k, int l, int r)
{
tree[k].left = l;
tree[k].right = r;
tree[k].len = r - l + 1;
// 一開始都是連通的,所以都是整個區間
tree[k].cntLeft = r - l + 1;
tree[k].cntRight = r - l + 1;
if (l == r)
return;
int mid = (l + r) / 2;
build(2 * k, l, mid);
build(2 * k + 1, mid + 1, r);
}
void change_point(int k, int x, int c)
{
if (tree[k].left == tree[k].right)
{
tree[k].cntLeft = tree[k].cntRight = c;
return;
}
int mid = (tree[k].left + tree[k].right) / 2;
if (mid >= x)
{
change_point(2 * k, x, c);
}
else
{
change_point(2 * k + 1, x, c);
}
// 如果左邊都是連續的,那麼需要加上右區間的左側連續長度
if (tree[2 * k].cntLeft == tree[2 * k].len)
{
tree[k].cntLeft = tree[2 * k].len + tree[2 * k + 1].cntLeft;
}
// 否則就是左邊的連續長度
else
{
tree[k].cntLeft = tree[2 * k].cntLeft;
}
// 如果右邊都是連續的,那麼需要加上左區間的右側連續長度
if (tree[2 * k + 1].cntRight == tree[2 * k + 1].len)
{
tree[k].cntRight = tree[2 * k].cntRight + tree[2 * k + 1].len;
}
// 否則就是右邊的連續長度
else
{
tree[k].cntRight = tree[2 * k + 1].cntRight;
}
}
void ask_point(int k, int x)
{
// 根節點需要特判,因爲同層只有這一個點
if (k == 1)
{
if (tree[k].cntLeft && tree[k].cntLeft + tree[k].left - 1 >= x)
{
ans = tree[k].cntLeft;
return;
}
if (tree[k].cntRight && tree[k].right - tree[k].cntRight + 1 <= x)
{
ans = tree[k].cntRight;
return;
}
}
// 葉子就退出
if (tree[k].left == tree[k].right)
{
return;
}
// 如果左側連續長度包含了該點
if (tree[k].cntLeft && tree[k].cntLeft + tree[k].left - 1 >= x)
{
ans = tree[k].cntLeft + tree[k - 1].cntRight;
return;
}
// 如果右側連續長度包含了該點
if (tree[k].cntRight && tree[k].right - tree[k].cntRight + 1 <= x)
{
ans = tree[k].cntRight + tree[k + 1].cntLeft;
return;
}
int mid = (tree[k].left + tree[k].right) / 2;
if (mid >= x)
{
ask_point(2 * k, x);
}
else
{
ask_point(2 * k + 1, x);
}
}
int main()
{
while (scanf("%d%d", &n, &m) == 2)
{
while (!pre.empty())
pre.pop();
build(1, 1, n);
for (int i = 0; i < m; i++)
{
getchar();
char ch;
scanf("%c", &ch);
if (ch == 'D')
{
int x;
scanf("%d", &x);
pre.push(x);
change_point(1, x, 0);
}
else if (ch == 'Q')
{
int x;
scanf("%d", &x);
ans = 0;
ask_point(1, x);
printf("%d\n", ans);
}
else
{
// 棧頂就是剛剛被摧毀的村莊序號
int x = pre.top();
pre.pop();
change_point(1, x, 1);
}
}
}
return 0;
}
1008:Atlantis(線段樹+掃描線)
題意:亞特蘭蒂斯中有 張地圖,每張地圖左上 和右下 兩個點描述了一個矩形區域,現在問這些區域的面積並是多少。
範圍:
分析:經典的線段樹掃描線題目了,不懂掃描線的翻我上面的鏈接。爲什麼這道題是 呢,因爲但凡涉及到浮點數總是會有點莫名其妙的錯誤,噁心!
詳見代碼。
Code:
#include <stdio.h>
#include <string>
#include <algorithm>
#include <iostream>
#define LL long long
using namespace std;
const int maxn = 210;
LL N;
double x[4 * maxn];
struct Edge
{
double l, r;
double h;
int flag; // 判斷是入邊還是出邊
bool operator<(Edge other) const
{
return h < other.h;
}
} edges[4 * maxn];
struct Node
{
LL l, r;
LL cnt;
double len;
} tree[4 * maxn];
LL findPos(LL l, LL r, double val)
{
LL mid;
while (l <= r)
{
mid = (l + r) >> 1;
if (x[mid] > val)
r = mid - 1;
else if (x[mid] < val)
l = mid + 1;
else
break;
}
return mid;
}
void build(LL rt, LL left, LL right)
{
tree[rt].l = left;
tree[rt].r = right;
tree[rt].len = 0;
tree[rt].cnt = 0;
if (left == right)
return;
LL mid = (left + right) >> 1;
build(rt << 1, left, mid);
build(rt << 1 | 1, mid + 1, right);
}
void pushUp(LL rt)
{
if (tree[rt].cnt) //非0,整段覆蓋
tree[rt].len = x[tree[rt].r + 1] - x[tree[rt].l];
else if (tree[rt].l == tree[rt].r) //葉子
tree[rt].len = 0;
else //部分覆蓋
tree[rt].len = tree[rt << 1].len + tree[rt << 1 | 1].len;
}
void update(LL rt, LL left, LL right, LL val)
{
if (left <= tree[rt].l && tree[rt].r <= right)
{ //全部包含
tree[rt].cnt += val;
pushUp(rt);
return;
}
LL mid = (tree[rt].l + tree[rt].r) >> 1;
if (left <= mid)
update(rt << 1, left, right, val);
if (right > mid)
update(rt << 1 | 1, left, right, val);
pushUp(rt); //計算該區間被覆蓋的總長度
}
int main()
{
LL K = 0;
LL l, r;
double x1, x2, y1, y2;
while (~scanf("%d", &N), N)
{
LL cnt = 0;
for (LL i = 1; i <= N; i++)
{
scanf("%lf%lf%lf%lf", &x1, &y1, &x2, &y2);
x[++cnt] = x1;
edges[cnt].l = x1;
edges[cnt].r = x2;
edges[cnt].h = y1;
edges[cnt].flag = 1; //下邊
x[++cnt] = x2;
edges[cnt].l = x1;
edges[cnt].r = x2;
edges[cnt].h = y2;
edges[cnt].flag = -1; //上邊
}
sort(x + 1, x + cnt + 1); //排序
sort(edges + 1, edges + cnt + 1);
// 這裏沒有去重操作 可以加上
build(1, 1, cnt);
double ans = 0;
for (LL i = 1; i <= cnt; i++)
{ //拿出每條橫線並且更新
l = findPos(1, cnt, edges[i].l);
r = findPos(1, cnt, edges[i].r) - 1;
update(1, l, r, edges[i].flag);
ans += tree[1].len * (edges[i + 1].h - edges[i].h); //求面積
}
printf("Test case #%d\n", ++K);
printf("Total explored area: %.2f\n\n", ans);
}
return 0;
}
1009:Paint the Wall(離散化+暴力)
題意:給一面 的牆,按順序在上面繪製 個帶有各種顏色的矩形圖案,用左上角和右下角的座標 來確定矩形的位置,矩形可能會重疊,顏色會被覆蓋,現在問全部繪製結束後各種顏色的矩形面積是多少?
範圍:
分析:本來想着這題該不會要用二維線段樹來做吧,確實是可以做的,但是在網上發現了更好的解法,比二維線段書又臭又長的代碼好多了,用的是離散化+暴力的解法。
由於範圍並不大,所以可以直接離散化後進行模擬。
表示 且 這個區域中的顏色。
按照輸入的順序給 數組賦值完之後重新統計數組中的顏色面積即可。
詳見代碼。
ZOJ 2747 Paint the Wall(離散化+暴力)題解
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000 + 10;
int h, w, n;
struct Node
{
int x1, y1, x2, y2;
int color;
} nodes[MAXN];
// x和y都是離散數組,ans保存答案
int x[MAXN], y[MAXN], ans[MAXN];
// g[i][j]表示這個x[i]~x[i+1]且y[i]~y[i+1]這塊區域的顏色
int g[MAXN][MAXN];
int main()
{
int kase = 1;
while (cin >> h >> w, h + w)
{
memset(g, 0, sizeof(g));
memset(ans, 0, sizeof(ans));
cin >> n;
int index1 = 0, index2 = 0;
// 離散化
for (int i = 0; i < n; i++)
{
cin >> nodes[i].x1 >> nodes[i].y1 >> nodes[i].x2 >> nodes[i].y2 >> nodes[i].color;
x[index1++] = nodes[i].x1;
x[index1++] = nodes[i].x2;
y[index2++] = nodes[i].y1;
y[index2++] = nodes[i].y2;
}
sort(x, x + index1);
sort(y, y + index2);
index1 = unique(x, x + index1) - x;
index2 = unique(y, y + index2) - y;
// 處理每塊區域的顏色
for (int i = 0; i < n; i++)
{
int x1 = lower_bound(x, x + index1, nodes[i].x1) - x;
int x2 = lower_bound(x, x + index1, nodes[i].x2) - x;
int y1 = lower_bound(y, y + index2, nodes[i].y1) - y;
int y2 = lower_bound(y, y + index2, nodes[i].y2) - y;
for (int j = x1; j < x2; j++)
{
for (int k = y1; k < y2; k++)
{
g[j][k] = nodes[i].color;
}
}
}
// 計算每塊顏色區域的面積
for (int i = 0; i < index1 - 1; i++)
{
for (int j = 0; j < index2 - 1; j++)
{
if (g[i][j])
ans[g[i][j]] += (x[i + 1] - x[i]) * (y[j + 1] - y[j]);
}
}
if (kase > 1)
cout << endl;
cout << "Case " << kase++ << ":" << endl;
int num = 0;
for (int i = 1; i <= 100; i++)
{
if (ans[i])
{
num++;
cout << i << " " << ans[i] << endl;
}
}
// 竟然還在這種小地方做手腳
if (num == 1)
{
cout << "There is 1 color left on the wall." << endl;
}
else
{
cout << "There are " << num << " colors left on the wall." << endl;
}
}
return 0;
}
1016:Can you answer these queries?(線段樹+優化)
題意:有 艘戰艦,每個戰艦有自己的初始耐力值 ,有 個操作,操作共兩種:(1),表示降低區間 之間戰艦的耐力值,;(2),表示查詢區間 之間戰艦的耐力值總和。
範圍:
分析:看起來就是簡單的線段樹區間修改以及區間查詢,但是直接上是會超時的,需要加一個優化:當子區間的耐力值爲 的時候就不需要更新了。在這道題中,這是個有效的優化,因爲對於一個整數只需要開根號幾次就會變成 ,不需要繼續向下進行更新。
詳見代碼。
Notice:注意開 long long;可能會出現 的情況,需要處理一下。
Code:
#include <iostream>
#include <cmath>
#include <cstring>
#include <algorithm>
#define LL long long
using namespace std;
const int maxn = 1e5 + 10;
LL ships[maxn], ans, col[4 * maxn];
LL n, m;
struct Node
{
LL left, right;
LL w;
} tree[4 * maxn];
void build(LL k, LL l, LL r)
{
tree[k].left = l;
tree[k].right = r;
if (l == r)
{
scanf("%lld", &tree[k].w);
return;
}
LL mid = (l + r) / 2;
build(2 * k, l, mid);
build(2 * k + 1, mid + 1, r);
tree[k].w = tree[2 * k].w + tree[2 * k + 1].w;
}
void change_interval(LL k, LL l, LL r)
{
if (tree[k].left == tree[k].right)
{
tree[k].w = (LL)sqrt(tree[k].w);
if (tree[k].w <= 1)
col[k] = 1;
return;
}
LL mid = (tree[k].left + tree[k].right) / 2;
// 已經被標記了就不走了
if (mid >= l && !col[2 * k])
change_interval(2 * k, l, r);
if (mid < r && !col[2 * k + 1])
change_interval(2 * k + 1, l, r);
tree[k].w = tree[2 * k].w + tree[2 * k + 1].w;
// 如果都是1,那麼這個大區間都是1
col[k] = col[2 * k] && col[2 * k + 1];
}
void ask_interval(LL k, LL l, LL r)
{
if (tree[k].left >= l && tree[k].right <= r)
{
ans += tree[k].w;
return;
}
LL mid = (tree[k].left + tree[k].right) / 2;
if (mid >= l)
ask_interval(2 * k, l, r);
if (mid < r)
ask_interval(2 * k + 1, l, r);
}
int main()
{
int kase = 1;
while (~scanf("%lld", &n))
{
printf("Case #%d:\n", kase++);
memset(col, 0, sizeof(col));
build(1, 1, n);
scanf("%lld", &m);
for (LL i = 0; i < m; i++)
{
LL T, x, y;
scanf("%lld%lld%lld", &T, &x, &y);
// 需要判斷 x > y的情況
if (x > y)
{
LL temp = x;
x = y;
y = temp;
}
if (T == 0)
{
change_interval(1, x, y);
}
else
{
ans = 0;
ask_interval(1, x, y);
printf("%lld\n", ans);
}
}
printf("\n");
}
return 0;
}
1017:Query(線段樹+區間合併)
題意:給兩個序列 和 ,有 個操作,操作共兩種:(1),表示將第 個串的第 位字符改成 ;(2),表示詢問從第 位開始兩個串匹配的長度。
範圍:
分析:問題實際上可以轉化爲 串上單點修改以及從某位開始連續 的長度。單點修改就不說了,連續 的長度可以使用區間合併的方式來求,總體代碼跟 差不多,需要改一點地方。
1006 求的是左右連續一整段的長度,這道題只要計算右邊一段的長度。
詳見代碼。
Code:
#include <iostream>
#include <cstdio>
#include <stack>
using namespace std;
const int maxn = 1e6 + 10;
int n1, n2, m, ans;
string str1, str2;
struct Node
{
int left, right;
int cntLeft, cntRight;
int len;
} tree[4 * maxn];
// 只需要右側的長度,左邊的減去
void pushUp(int k)
{
if (tree[2 * k].cntLeft == tree[2 * k].len)
{
tree[k].cntLeft = tree[2 * k].len + tree[2 * k + 1].cntLeft;
}
else
{
tree[k].cntLeft = tree[2 * k].cntLeft;
}
if (tree[2 * k + 1].cntRight == tree[2 * k + 1].len)
{
tree[k].cntRight = tree[2 * k].cntRight + tree[2 * k + 1].len;
}
else
{
tree[k].cntRight = tree[2 * k + 1].cntRight;
}
}
void build(int k, int l, int r)
{
tree[k].left = l;
tree[k].right = r;
tree[k].len = r - l + 1;
if (l == r)
{
// 相同爲1, 否則爲0
if (str1[l] == str2[l])
{
tree[k].cntLeft = tree[k].cntRight = 1;
}
else
{
tree[k].cntLeft = tree[k].cntRight = 0;
}
return;
}
int mid = (l + r) / 2;
build(2 * k, l, mid);
build(2 * k + 1, mid + 1, r);
pushUp(k);
}
void change_point(int k, int x, int c)
{
if (tree[k].left == tree[k].right)
{
tree[k].cntLeft = tree[k].cntRight = c;
return;
}
int mid = (tree[k].left + tree[k].right) / 2;
if (mid >= x)
{
change_point(2 * k, x, c);
}
else
{
change_point(2 * k + 1, x, c);
}
pushUp(k);
}
// 區間合併常規操作,只要右側長度
void ask_point(int k, int x)
{
if (k == 1)
{
if (tree[k].cntLeft && tree[k].cntLeft + tree[k].left - 1 >= x)
{
ans = tree[k].left + tree[k].cntLeft - x;
return;
}
if (tree[k].cntRight && tree[k].right - tree[k].cntRight + 1 <= x)
{
ans = tree[k].len - x;
return;
}
}
if (tree[k].left == tree[k].right)
{
return;
}
if (tree[k].cntLeft && tree[k].cntLeft + tree[k].left - 1 >= x)
{
ans = tree[k].left + tree[k].cntLeft + tree[k - 1].cntRight - x;
return;
}
if (tree[k].cntRight && tree[k].right - tree[k].cntRight + 1 <= x)
{
ans = tree[k].len - x + tree[k + 1].cntLeft;
return;
}
int mid = (tree[k].left + tree[k].right) / 2;
if (mid >= x)
{
ask_point(2 * k, x);
}
else
{
ask_point(2 * k + 1, x);
}
}
int main()
{
int T;
cin >> T;
int kase = 1;
while (T--)
{
cout << "Case " << kase++ << ":" << endl;
cin >> str1 >> str2;
// 注意兩個字符串長度可以不一樣
n1 = str1.length();
n2 = str2.length();
build(1, 0, min(n1 - 1, n2 - 1));
cin >> m;
for (int i = 0; i < m; i++)
{
int op;
cin >> op;
if (op == 1)
{
int a, b;
char c;
cin >> a >> b >> c;
if (a == 1)
str1[b] = c;
else
str2[b] = c;
// 當前位置必須都有字符且相同才修改
if (n1 > b && n2 > b && str1[b] == str2[b])
change_point(1, b, 1);
}
else
{
int x;
cin >> x;
ans = 0;
ask_point(1, x);
cout << ans << endl;
}
// show();
}
}
return 0;
}
三、hard
1001:Brownie Points II(雙線段樹+離散化)
題意:平面上有 個位置不同的點 ,玩家 選擇一條穿過某些點(一個或多個)的垂直線,玩家 再選擇一條穿過一個點(同時被垂直線穿過)的水平線,這樣兩條線把平面劃分成四個象限,一、三象限中點的數量就是 的得分,二、四象限中的點的數量則是 的得分,線上的點忽略。假設 一定會選擇當前局面下的最優解,問 能得到的最大分數以及在該情況下 可能的得分。
範圍:, 和 是整數
分析: 細節很多,想了挺久,寫了更久,還好一發就過了。
按照題目的意思我們可以發現我們只需要求得以每個點作爲座標原點時雙方的答案,得到 能獲得的最優解以及此時 的所有可行解。
先手只能選 軸,後手必定會選最優解,而我們要在後手使用最優解的情況下讓自己答案儘可能大。
我們將所有點的 座標進行分組,同一 座標下可能會有多個點,組數不會超過 。
同一組中 的最優解可能有多個(真的嗎?),因此我們要求每一組中 所有最優解中本方的最小值 ,那麼 的最優解就是所有組中最大的 。
那麼現在的問題就轉化成以點 作爲座標原點,如何快速統計雙方的分數?我是這樣做的,可能有點麻煩。
將所有的點按照 座標從小到大排序, 相同則以 座標從大到小排序,並且將所有點的 座標進行離散化, 那麼此時離散化後的 座標不會超過 個。
以離散化後的 座標創建兩個線段樹,分別代表左側的線段樹以及右側的線段樹,線段樹上的點 代表 座標爲 的點的個數, 表示離散化後 軸上第 個點的真實 座標。
一開始把所有的點都加入右側的線段樹,隨着我們掃描每組點,將點逐步加入到左側的線段樹。
那麼對於每個點,我們可以利用左側線段樹的左區間+右側線段樹的右區間得到自身可以得到的分數,對手則是左側線段樹的右區間+右側線段樹的左區間,即每個人都進行兩次區間查詢!
這樣我們就可以求出雙方的分數,當對手遇到更優解時重置自身在這組點中的最小值,遇到相同最優解時則更新最小值。
當處理完一組點之後,如果這一組的最小值 比答案 大,那麼更新答案,將 清空,加入 的可行解;如果跟答案相同,則把對方的當前可行解加入 。
總體時間複雜度 ,詳見代碼。
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 2e5 + 10;
const int INF = 0x3f3f3f3f;
struct Node
{
int l, r;
int sum;
} ltree[4 * MAXN], rtree[4 * MAXN];
struct Point
{
int x, y;
bool operator<(Point other) const
{
if (x != other.x)
return x < other.x;
else
return y > other.y;
}
} p[MAXN];
int n, ans;
int lisan[MAXN]; // y座標的離散化數組
vector<int> temp; // 保存當前組中的點
set<int> ansList; // 保存B的所有可行解
map<int, int> numX; // 記錄當前組中剩餘的點個數
// 左側線段樹建樹
void lbuild(int rt, int l, int r)
{
ltree[rt].l = l, ltree[rt].r = r;
ltree[rt].sum = 0;
if (l == r)
{
return;
}
int mid = l + r >> 1;
lbuild(2 * rt, l, mid);
lbuild(2 * rt + 1, mid + 1, r);
}
// 右側線段樹建樹
void rbuild(int rt, int l, int r)
{
rtree[rt].l = l, rtree[rt].r = r;
rtree[rt].sum = 0;
if (l == r)
{
return;
}
int mid = l + r >> 1;
rbuild(2 * rt, l, mid);
rbuild(2 * rt + 1, mid + 1, r);
}
// 左側線段樹合併信息,維護簡單區間和
void lpushUp(int rt)
{
ltree[rt].sum = ltree[2 * rt].sum + ltree[2 * rt + 1].sum;
}
// 右側線段樹合併信息,維護簡單區間和
void rpushUp(int rt)
{
rtree[rt].sum = rtree[2 * rt].sum + rtree[2 * rt + 1].sum;
}
// 左側線段樹單點修改
void lchange_point(int rt, int x, int c)
{
if (ltree[rt].l == ltree[rt].r)
{
ltree[rt].sum += c;
return;
}
int mid = ltree[rt].l + ltree[rt].r >> 1;
if (x <= mid)
lchange_point(2 * rt, x, c);
else
lchange_point(2 * rt + 1, x, c);
lpushUp(rt);
}
// 右側線段樹單點修改
void rchange_point(int rt, int x, int c)
{
if (rtree[rt].l == rtree[rt].r)
{
rtree[rt].sum += c;
return;
}
int mid = rtree[rt].l + rtree[rt].r >> 1;
if (x <= mid)
rchange_point(2 * rt, x, c);
else
rchange_point(2 * rt + 1, x, c);
rpushUp(rt);
}
// 左側線段樹區間查詢
void lask_interval(int rt, int l, int r)
{
if (ltree[rt].l >= l && ltree[rt].r <= r)
{
ans += ltree[rt].sum;
return;
}
int mid = ltree[rt].l + ltree[rt].r >> 1;
if (l <= mid)
lask_interval(2 * rt, l, r);
if (mid < r)
lask_interval(2 * rt + 1, l, r);
}
// 右側線段樹區間查詢
void rask_interval(int rt, int l, int r)
{
if (rtree[rt].l >= l && rtree[rt].r <= r)
{
ans += rtree[rt].sum;
return;
}
int mid = rtree[rt].l + rtree[rt].r >> 1;
if (l <= mid)
rask_interval(2 * rt, l, r);
if (mid < r)
rask_interval(2 * rt + 1, l, r);
}
int main()
{
while (cin >> n, n)
{
// 清空容器
temp.clear();
ansList.clear();
numX.clear();
int index = 0;
for (int i = 0; i < n; i++)
{
cin >> p[i].x >> p[i].y;
// 先放入離散數組
lisan[index++] = p[i].y;
// 更新該組剩餘點數量
numX[p[i].x]++;
}
// 離散化先排序
sort(lisan, lisan + index);
// 離散化去重,完成離散化
index = unique(lisan, lisan + index) - lisan;
// 建立左右線段樹
lbuild(1, 0, n - 1);
rbuild(1, 0, n - 1);
// 先把所有點加入右側線段樹
for (int i = 0; i < n; i++)
{
// 二分查找該y座標對應的離散化序號
int pos = lower_bound(lisan, lisan + index, p[i].y) - lisan;
rchange_point(1, pos, 1);
}
// 對所有點進行排序
sort(p, p + n);
// minV是該組中A能得到的最小值,maxV是該組中B能得到的最大值,res是所有組中最大的minV
int minV = INF, maxV = 0, res = 0;
for (int i = 0; i < n; i++)
{
// 剩餘數量減少
numX[p[i].x]--;
// 如果到了新的一組,那麼上一組的所有點需要加入到左側的線段樹
if (i - 1 >= 0 && p[i - 1].x != p[i].x)
{
for (auto v : temp)
{
int pos = lower_bound(lisan, lisan + index, p[v].y) - lisan;
lchange_point(1, pos, 1);
}
temp.clear();
// 更新答案
if (res < minV)
{
res = minV;
ansList.clear();
ansList.insert(maxV);
}
else if (res == minV)
{
ansList.insert(maxV);
}
// 重置最值
minV = INF;
maxV = 0;
}
// temp一個個加入該組的所有點
temp.push_back(i);
// 從右樹中刪除該點
int pos = lower_bound(lisan, lisan + index, p[i].y) - lisan;
rchange_point(1, pos, -1);
// 計算左上與右下的點數,注意不要越界
int tempAns = 0;
if (pos + 1 < index)
{
ans = 0;
lask_interval(1, pos + 1, index - 1);
tempAns += ans;
}
if (pos - 1 >= 0)
{
ans = 0;
rask_interval(1, 0, pos - 1);
tempAns += ans;
}
// 計算右下的時候該組下方的點也被記錄在內,所以需要扣除該組剩餘點數
tempAns -= numX[p[i].x];
// 如果B出現了更優解或同優解時才需要計算A的得分
if (tempAns >= maxV)
{
// 出現更優解則之前計算的minV就沒用了
if (tempAns > maxV)
minV = INF;
maxV = tempAns;
tempAns = 0;
if (pos + 1 < index)
{
ans = 0;
rask_interval(1, pos + 1, index - 1);
tempAns += ans;
}
if (pos - 1 >= 0)
{
ans = 0;
lask_interval(1, 0, pos - 1);
tempAns += ans;
}
// 更新該組A能得到的最小值
minV = min(minV, tempAns);
}
}
// 還需要處理一下最後一組
if (res < minV)
{
res = minV;
ansList.clear();
ansList.insert(maxV);
}
else if (res == minV)
{
ansList.insert(maxV);
}
cout << "Stan: " << res << "; Ollie:";
for (auto x : ansList)
{
cout << " " << x;
}
cout << ";" << endl;
}
return 0;
}
1013:Harmony Forever(線段樹+分情況處理)
題意:有一個空集合 ,有 個操作,操作共兩種:(1) ,表示將 加入集合 ;(2),表示查詢集合 中 的最大值。
範圍:
分析:這題不好想,確實是好題!
如果操作(2)要查詢的是集合中的最大值,那就是水題了,現在需要 ,那該怎麼辦呢?
這裏採取分組的辦法,將集合中的元素按照 劃分成一個一個子區間,比如 ,那麼我們要求 中 的最大值,相當於我們需要在這些子區間中進行區間查詢,找到每個子區間的最小值,然後再取其中的最大值。那麼這樣本題就可以轉化成爲線段樹單點修改與區間查詢的問題了。
但是還有一個問題,如果就這樣直接上線段樹肯定會超時的,因爲上面的想法是每個查詢對集合進行分組然後再對每個分組進行區間查詢,如果 的話,區間查詢就退化成單點查詢,此時我們需要對 個元素進行單點查詢,鐵定超時!
所以我們需要對 的值進行判斷,當 比較小的時候我們可以直接暴力遍歷集合 來找到最優解,當 比較大的時候線段樹的優勢就出來了,在本題中 使用暴力,其他情況使用線段樹就可以了。
詳見代碼。
參考自
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 5e5 + 10;
const int INF = 0x3f3f3f3f;
int n, ans;
int idx, minV, maxV;
// arr模擬集合S,last表示數字最新出現的位置
int arr[MAXN], last[MAXN];
struct Node
{
int l, r, minV;
} tree[4 * MAXN];
void pushUp(int k)
{
tree[k].minV = min(tree[2 * k].minV, tree[2 * k + 1].minV);
}
void build(int k, int l, int r)
{
tree[k].l = l, tree[k].r = r;
tree[k].minV = INF;
if (l == r)
return;
int mid = l + r >> 1;
build(2 * k, l, mid);
build(2 * k + 1, mid + 1, r);
}
void change_point(int k, int x)
{
if (tree[k].l == tree[k].r)
{
tree[k].minV = x;
return;
}
int mid = tree[k].l + tree[k].r >> 1;
if (x <= mid)
change_point(2 * k, x);
else
change_point(2 * k + 1, x);
pushUp(k);
}
void ask_interval(int k, int l, int r)
{
if (tree[k].l >= l && tree[k].r <= r)
{
ans = min(ans, tree[k].minV);
return;
}
int mid = tree[k].l + tree[k].r >> 1;
if (l <= mid)
ask_interval(2 * k, l, r);
if (mid < r)
ask_interval(2 * k + 1, l, r);
}
// 線性暴力查詢
int lineSearch(int x)
{
// cout << "idx: " << idx << endl;
int mmin = INF, num;
for (int i = idx - 1; i >= 0; i--)
{
// cout << "arr: " << arr[i] % x << endl;
if (arr[i] % x < mmin)
{
mmin = arr[i] % x;
num = arr[i];
if (mmin == 0)
break;
}
}
// cout << "last: " << last[num] << endl;
return last[num];
}
// 線段樹查詢
int treeSearch(int x)
{
// 如果比當前的最大值還大,那麼直接返回最小值即可
if (x > maxV)
return last[minV];
// mmin表示mod之後的最小值,num表示實際最小值
int mmin = INF, num;
for (int i = 0; i <= maxV; i += x)
{
// 注意r不要越界
int l = i, r = min(i + x - 1, maxV);
ans = INF;
ask_interval(1, l, r);
// 如果該區間沒有滿足條件的則跳過
if (ans >= INF)
continue;
// 更新答案
if (mmin > ans % x)
{
mmin = ans % x;
num = ans;
}
else if (mmin == ans % x)
{
if (last[num] < last[ans])
{
num = ans;
}
}
}
return last[num];
}
int main()
{
int kase = 1;
while (cin >> n, n)
{
if (kase != 1)
cout << endl;
cout << "Case " << kase++ << ":" << endl;
idx = 0;
minV = INF;
maxV = 0;
build(1, 0, 500000);
for (int i = 0; i < n; i++)
{
string str;
int x;
cin >> str >> x;
if (str == "B")
{
arr[idx++] = x;
last[x] = idx;
change_point(1, x);
minV = min(minV, x);
maxV = max(maxV, x);
}
else
{
// 如果沒有元素,那麼出錯
if (idx == 0)
{
cout << -1 << endl;
continue;
}
int res;
// 如果數字比較小,那麼就直接暴力
if (x <= 5000)
{
res = lineSearch(x);
}
// 否則就上線段樹分組進行區間查詢
else
{
res = treeSearch(x);
}
cout << res << endl;
}
}
}
return 0;
}
1014:Turing Tree(線段樹+離線+map)
題意:給一個長度爲 的序列 , 還有 個詢問,每個詢問 表示查詢區間 中不重複數字之和。
範圍:
分析:線段樹離線處理經典題了,需要好好掌握。
問題的難點就在於不重複,對於每個詢問區間中的數字我們不好通過線段樹結點的信息來得知這個區間中是否重複過了,那麼我們就可以對查詢的處理順序做文章。
我們先把所有的查詢區間離線保存下來,按照區間的右邊界進行排序。從左到右遍歷這個序列,記錄下該數字 最新的出現位置,新位置的權值置爲 ,舊位置的權值置爲 ,由於數字值域範圍很大,所以需要使用 來記錄,當處理到了某個區間的右邊界,那麼我們就可以使用區間查詢得到該區間中不重複數字之和。由於區間是按照右邊界進行排序的,所以將舊位置權值置爲 不會對後面區間答案的正確性造成影響。
這樣我們就可以得到所有區間的答案,處理完之後我們再按照查詢的輸入順序重新排序,輸出答案即可。
Notice:記得開 long long
Code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN = 3e4 + 10;
const int MAXM = 1e5 + 10;
int n, q, ans;
int arr[MAXN];
map<int, int> mp; // mp映射一個數字最新出現的位置
struct Node
{
int l, r, sum;
} tree[4 * MAXN];
// 查詢結構體
struct Qry
{
int l, r, idx, ans;
} qry[MAXM];
// 按照右邊界排序
bool cmp1(Qry a, Qry b)
{
if (a.r != b.r)
return a.r < b.r;
else
return a.l < b.l;
}
// 按照輸入順序排序
bool cmp2(Qry a, Qry b)
{
return a.idx < b.idx;
}
void build(int k, int l, int r)
{
tree[k].l = l, tree[k].r = r;
tree[k].sum = 0;
if (l == r)
return;
int mid = l + r >> 1;
build(2 * k, l, mid);
build(2 * k + 1, mid + 1, r);
}
void pushUp(int k)
{
tree[k].sum = tree[2 * k].sum + tree[2 * k + 1].sum;
}
void change_point(int k, int x, int c)
{
if (tree[k].l == tree[k].r)
{
tree[k].sum = c;
return;
}
int mid = tree[k].l + tree[k].r >> 1;
if (x <= mid)
change_point(2 * k, x, c);
else
change_point(2 * k + 1, x, c);
pushUp(k);
}
void ask_interval(int k, int l, int r)
{
if (tree[k].l >= l && tree[k].r <= r)
{
ans += tree[k].sum;
return;
}
int mid = tree[k].l + tree[k].r >> 1;
if (l <= mid)
ask_interval(2 * k, l, r);
if (mid < r)
ask_interval(2 * k + 1, l, r);
}
signed main()
{
int T;
cin >> T;
while (T--)
{
// 注意清空
mp.clear();
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> arr[i];
}
// 離線輸入
cin >> q;
for (int i = 0; i < q; i++)
{
cin >> qry[i].l >> qry[i].r;
qry[i].idx = i;
}
sort(qry, qry + q, cmp1);
build(1, 1, n);
int now = 1;
// 總共會碰到q次右邊界
for (int i = 0; i < q; i++)
{
// 沒有碰到右邊界則一直更新
while (now <= qry[i].r)
{
// 存在,則把舊的賦值爲0
if (mp.count(arr[now]))
{
change_point(1, mp[arr[now]], 0);
}
change_point(1, now, arr[now]);
mp[arr[now]] = now;
now++;
}
// 進行區間查詢,得到該查詢的答案
ans = 0;
ask_interval(1, qry[i].l, qry[i].r);
qry[i].ans = ans;
}
// 重新排序,輸出答案
sort(qry, qry + q, cmp2);
for (int i = 0; i < q; i++)
{
cout << qry[i].ans << endl;
}
}
return 0;
}
1015:Orienteering(LCS 轉 LIS)
題意:某個大學有個 個校區,此大學有 個運動員,這 個運動員在每個校區都挑選了 個拉拉隊。現在每個校區(A/B)中,這 個拉拉隊按照登記順序說出自己支持的運動員編號 和自己想排在那個位置 ,排成一列。如果衝突則按照先來後到的順序依次往後排。求按照A/B的兩個拉拉隊員站的位置,用他們支持的運動員編號形成的兩個序列的最長公共子序列。
範圍:
分析:有兩個子問題需要處理,第一個就是需要得到兩個序列,他們之間會出現衝突,他們的位置可以使用二分來解決,這個好辦。困難的是第二個問題,求兩個序列的最長公共子序列,當然直接上 算法肯定是不行的,其實這種問題有專門的名字:稀疏序列匹配。稀疏序列匹配的問題可以通過處理轉化成區間最值問題,然後使用線段樹/樹狀數組等數據結構來解決,但是這道題目時間卡得比較緊,所以線段樹比較難寫!不過這題有更有意思的解法,就是將 問題轉化成 問題。這個思維的轉換確實可以學習一下。
具體怎麼做呢?先得到我們需要處理的兩個序列 和 ,我們可以得到 中每個元素在 中所有匹配的位置,將這些位置倒敘保存起來,然後將所有元素倒敘保存的位置拼接起來,跑 即可!
沒懂?先把問題簡化一下,假設序列 和 內部中的元素不重複,那麼怎麼求 呢?因爲 不要求子序列連續,只需要保證序號要遞增,那麼我們是不是就可以求出 中元素在 中匹配的位置,形成序列,而我們要求 ,就要求這個序列中最長的遞增子序列,即 。
現在再考慮重複的問題, 中每個元素可能對應 中多個位置,如果直接跑 會導致 中一個元素跟 中元素形成多次匹配,導致答案不正確,那麼怎麼消除這個影響呢?就是把 中每個元素在 中的匹配序列進行倒置,那麼就保證一個元素自身的匹配序列是遞減的,不會形成多次匹配的情況。
所以我們需要把所有元素的匹配序列進行倒敘之後拼接,這樣跑 就是我們要求的 。
詳見代碼。
參考自
https://www.cnblogs.com/wonderzy/p/3434269.html
Code:
#include <iostream>
#include <cstdio>
#include <map>
#include <vector>
#include <cstring>
#include <set>
#include <algorithm>
#define maxn 200010
using namespace std;
int t, n, m;
int fa[maxn], fb[maxn];
vector<int> za, zb;
vector<int> w[10010];
set<int> qa, qb;
int v[maxn * 5], ct;
void init()
{
za.clear(), zb.clear();
memset(fa, 0, sizeof fa);
memset(fb, 0, sizeof fb);
qa.clear(), qb.clear();
for (int i = 0; i < 10010; i++)
w[i].clear();
}
void read()
{
scanf("%d%d", &n, &m);
int c, x;
for (int i = 1; i <= 2 * n * m; i++)
qa.insert(i);
for (int i = 1; i <= 2 * n * m; i++)
qb.insert(i);
for (int i = 0; i < n * m; i++)
{
scanf("%d%d", &c, &x);
set<int>::iterator at = qa.lower_bound(x);
if (at != qa.end())
{
fa[*at] = c;
qa.erase(at);
}
}
for (int i = 0; i < n * m; i++)
{
scanf("%d%d", &c, &x);
set<int>::iterator at = qb.lower_bound(x);
if (at != qb.end())
{
fb[*at] = c;
qb.erase(at);
}
}
for (int i = 1; i <= 2 * m * n; i++)
{
if (fa[i])
za.push_back(fa[i]);
}
for (int i = 0; i < n * m; i++)
{
w[za[i]].push_back(i + 1);
}
for (int i = 1; i <= 2 * m * n; i++)
{
if (fb[i])
zb.push_back(fb[i]);
}
}
int gao_LIS(int a[], int len)
{
int ret = 0;
int b[maxn];
b[ret++] = a[0];
for (int i = 1; i < len; i++)
{
int x = lower_bound(b, b + ret, a[i]) - b;
if (x == ret)
{
b[ret++] = a[i];
}
else
{
b[x] = a[i];
}
}
return ret;
}
void solve(int ca)
{
ct = 0;
for (int i = 0; i < n * m; i++)
{
int nn = w[zb[i]].size();
for (int j = nn - 1; j >= 0; j--)
{
v[ct++] = w[zb[i]][j];
}
}
printf("Case #%d: %d\n", ca, gao_LIS(v, ct));
}
int main()
{
scanf("%d", &t);
for (int ca = 1; ca <= t; ca++)
{
init();
read();
solve(ca);
}
return 0;
}
1019:Lamp(Dancing links 舞蹈鏈)
題意:房間裏面有 盞燈和 個開關,一盞燈可以由多個開關控制,一個開關最多隻能控制兩盞燈,現在問是否能夠通過打開某些開關和關閉某些開關使所有的燈亮起來。
範圍:
分析:經典的舞蹈鏈問題,利用交叉十字循環雙向鏈表實現的精確覆蓋/重複覆蓋問題的算法,舞蹈鏈我這裏就不說了吧,篇幅實在太長了,現在不會也沒關係,這種數據結構應該我們後續的訓練也會涉及到。
有興趣的同學 click 這裏:Dancing links——DLX搜索詳解
Code:
#include <bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f3f
#define eps 1e-8
#define pi acos(-1.0)
typedef long long ll;
struct DLX
{
const static int maxn = 200010;
#define FF(i, A, s) for (int i = A[s]; i != s; i = A[i])
int L[maxn], R[maxn], U[maxn], D[maxn];
int size, col[maxn], row[maxn], s[maxn], H[maxn];
bool vis[1200];
int ans[maxn], cnt;
void init(int m)
{
for (int i = 0; i <= m; i++)
{
L[i] = i - 1;
R[i] = i + 1;
U[i] = D[i] = i;
s[i] = 0;
}
memset(H, -1, sizeof(H));
L[0] = m;
R[m] = 0;
size = m + 1;
memset(vis, 0, sizeof(vis));
}
void link(int r, int c)
{
U[size] = c;
D[size] = D[c];
U[D[c]] = size;
D[c] = size;
if (H[r] < 0)
H[r] = L[size] = R[size] = size;
else
{
L[size] = H[r];
R[size] = R[H[r]];
L[R[H[r]]] = size;
R[H[r]] = size;
}
s[c]++;
col[size] = c;
row[size] = r;
size++;
}
void del(int c)
{ //精確覆蓋
L[R[c]] = L[c];
R[L[c]] = R[c];
FF(i, D, c)
FF(j, R, i)
U[D[j]] = U[j],
D[U[j]] = D[j], --s[col[j]];
}
void add(int c)
{ //精確覆蓋
R[L[c]] = L[R[c]] = c;
FF(i, U, c)
FF(j, L, i)
++s[col[U[D[j]] = D[U[j]] = j]];
}
bool dfs(int k)
{ //精確覆蓋
if (!R[0])
{
cnt = k;
return 1;
}
int c = R[0];
FF(i, R, 0)
if (s[c] > s[i])
c = i;
del(c);
FF(i, D, c)
{
FF(j, R, i)
del(col[j]);
ans[k] = row[i];
if (dfs(k + 1))
return true;
FF(j, L, i)
add(col[j]);
}
add(c);
return 0;
}
void remove(int c)
{ //重複覆蓋
FF(i, D, c)
L[R[i]] = L[i],
R[L[i]] = R[i];
}
void resume(int c)
{ //重複覆蓋
FF(i, U, c)
L[R[i]] = R[L[i]] = i;
}
int A()
{ //估價函數
int res = 0;
memset(vis, 0, sizeof(vis));
FF(i, R, 0)
if (!vis[i])
{
res++;
vis[i] = 1;
FF(j, D, i)
FF(k, R, j)
vis[col[k]] = 1;
}
return res;
}
bool dance(int now)
{ //重複覆蓋
if (R[0] == 0)
return 1;
int temp = INF, c;
FF(i, R, 0)
if (temp > s[i])
temp = s[i],
c = i;
FF(i, D, c)
{
if (vis[row[i] ^ 1])
continue;
vis[row[i]] = 1;
remove(i);
FF(j, R, i)
remove(j);
if (dance(now + 1))
return 1;
FF(j, L, i)
resume(j);
resume(i);
vis[row[i]] = 0;
}
return 0;
}
} dlx;
int main()
{
int n, m;
while (~scanf("%d%d", &n, &m))
{
dlx.init(n);
for (int i = 1; i <= n; i++)
{
int a, b;
char str[44];
scanf("%d", &a);
while (a--)
{
scanf("%d%s", &b, str);
if (str[1] == 'N')
dlx.link((b - 1) << 1, i);
else
dlx.link((b - 1) << 1 | 1, i);
}
}
if (!dlx.dance(0))
puts("-1");
else
{
if (!dlx.vis[1])
printf("ON");
else
printf("OFF");
for (int i = 2; i < (m << 1); i += 2)
{
if (!dlx.vis[i])
printf(" OFF");
else
printf(" ON");
}
puts("");
}
}
return 0;
}
【END】感謝觀看