WHUT第十一周训练整理

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
题意:有 N N N 个工兵营地,营地人数为 a i a_i ai, 有若干条命令,共 4 4 4 种形式:(1) A d d   i   j   Add~i~j~ Add i j ,表示 i i i 营地增加 j j j 个人;(2) S u b   i   j   Sub~i~j~ Sub i j ,表示 i i i 营地减少 j j j 个人;(3) Q u e r y   i   j   Query~i~j~ Query i j ,查询营地 i i i 到营地 j j j 中的总人数;(4) E n d ​ End​ End,命令结束。

范围: N ≤ 50000   ,   1 ≤ a i ≤ 50 N \le 50000~,~1 \le a_i \le 50 N50000 , 1ai50

分析:线段树板子题,根据给的操作维护一个区间和,进行单点修改以及区间查询即可,常规操作。

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(线段树+扫描线)

题意:给若干个矩形的两个对角点 ( x i , y i ) (x_i, y_i) (xi,yi),求整个图形的面积。

范围: 0 ≤ x i , y i ≤ 100 0 \le x_i, y_i \le 100 0xi,yi100

分析:能做出 1003 的这道题肯定没有问题,这道题目甚至都不需要离散化,直接对区间 [ 0 , 100 ] [0, 100] [0,100] 进行建树跑扫描线即可。

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(逆序对+预处理)

题意:给一个 [ 0 , N − 1 ] ​ [0,N-1]​ [0,N1] 的排列,可以循环移动让任意一个数字打头,问最小的逆序数是多少?

范围: N ≤ 5000 N \le 5000 N5000

分析:数据范围不大,不需要使用线段树/树状数组等数据结构。

直接先双重循环求出原始序列的逆序对,预处理出数组 l l l r r r,分别表示数字 i i i 左右两边比 i i i 大的数字的个数。

每当一个数字移动到最后时,原先右侧的数字与自身的逆序关系发生了翻转,那么逆序数就改变了 r [ i ] − ( n − r − i − 1 ) = 2 ∗ r [ i ] − n − i + 1 r[i]-(n-r-i-1) = 2*r[i]-n-i+1 r[i](nri1)=2r[i]ni+1

而左侧的数字在前面的操作中已经移动到右侧,当前数字移动到右侧之后与这些数字的逆序关系恢复,改变了 l [ i ] − ( i − l [ i ] ) = 2 ∗ l [ i ] + i l[i]-(i-l[i]) = 2*l[i]+i l[i](il[i])=2l[i]+i

那么把上面的答案合并一下,改变量 = 2 ∗ ( l [ i ] + r [ i ] ) − n + 1 = 2*(l[i]+r[i])-n+1 =2(l[i]+r[i])n+1

于是模拟一下移动的过程更新答案即可。

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(线段树)

题意:有天空中 N N N 颗星星的位置 ( x i , y i ) (x_i, y_i) (xi,yi),定义星星的等级为其左下方星星的数量(不包括自己),问每种等级的星星各有多少。

范围: 1 ≤ N ≤ 15000   ,   0 ≤ x i , y i ≤ 32000 1 \le N \le 15000~,~0 \le x_i, y_i \le 32000 1N15000 , 0xi,yi32000

分析:我们需要统计的是每个星星左下角的星星数量,我们可以先将所有的星星按照坐标 x x x 轴进行排序,那么接下来扫描每个点,我们要求的就是之前高度小于当前点的星星数量,那么问题就好办了。因为 y y y 坐标的值域不大,所以不需要离散化,直接使用线段树对 y y y 坐标进行建树,每次扫描到一个点,把这个点的 y ​ y​ y 坐标加入树中,那么对所有点我们就只需要对线段树进行一次区间查询即可。

详见代码。

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(线段树)

题意:有 N N N 个编号为 1... N 1...N 1...N 的气球,同时进行 N N N 次操作,每次给编号在 [ a , b ] [a, b] [a,b] 中的连续气球进行统一涂色,全部操作结束之后问每个气球被涂色的总次数。

范围: N ≤ 100000   ,   1 ≤ a ≤ b ≤ N N \le 100000~, ~1 \le a \le b \le N N100000 , 1abN

分析:线段树板子题,区间修改,单点查询。

详见代码。

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(线段树)

题意:有一根长度为 N N N 的钩子,进行 Q Q Q 次操作,每次将 [ X , Y ] [X, Y] [X,Y] 这一段钩子的材质变成 Z Z Z,问全部操作结束后钩子的总材质值。

范围: 1 ≤ N ≤ 100000   ,   0 ≤ Q ≤ 100000   ,   1 ≤ X ≤ Y ≤ N   ,   Z ∈ { 1 , 2 , 3 } 1 \le N \le 100000~,~0 \le Q \le 100000~,~1 \le X \le Y \le N~,~Z \in \{1, 2, 3\} 1N100000 , 0Q100000 , 1XYN , Z{1,2,3}

分析:线段树板子题,只需要区间修改即可。

详见代码。

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(线段树)

题意:有 N N N 个同学(编号为 1... N 1...N 1...N)的成绩,且有 M M M 个操作,操作共两种:(1) Q   A   B Q~A~B Q A B,表示查询编号区间为 [ A , B ] [A, B] [A,B] 中的成绩最高值;(2) U   A   B U~A~B U A B,表示将同学 A A A 的成绩改成 B B B

范围: 0 < N ≤ 200000   ,   0 < M < 5000 0 < N \le 200000~,~0 < M < 5000 0<N200000 , 0<M<5000

分析:线段树板子题,区间查询,单点修改,维护一个最大值。

详见代码。

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(线段树+贪心)

题意:有 N N N 个开着或关着的灯笼排成一排,现在有一种开关可以选择从任意位置开始控制连续 k k k 个灯笼的开关状态,让这 k k k 个灯笼的状态全部反转,现在要至少需要多少个这样的开关才能让所有灯笼全亮,没有可行解则输出 − 1 ​ -1​ 1

范围: 0 < N ≤ 100000   ,   0 ≤ k ≤ N 0 < N \le 100000~,~0 \le k \le N 0<N100000 , 0kN

分析:我们需要让所有的灯亮起来,那么我们从左往右考虑的话,如果当前的灯是暗的,那么我们就需要在这个位置安装一个开关来将这个灯点亮看,这个时候我们已经假设前面的灯已经通过前面的开关全部点亮了,此时需要改变当前灯的状态只能重新安装一个开关,因此就用这样的想法使用线段树来进行区间修改模拟,最后再对每个点进行单点查询检查是否全部点亮即可。

详见代码。

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:覆盖的面积(线段树+扫描线)

题意:给 N N N 个矩形的左下与右上顶点坐标 ( x i , y i ) (x_i, y_i) (xi,yi),矩形的边与坐标轴平行,求出被这些矩形覆盖过至少两次的区域的面积。

范围: 1 ≤ N ≤ 1000   ,   0 ≤ x i , y i ≤ 100000 1\le N \le 1000~,~0\le x_i, y_i \le 100000 1N1000 , 0xi,yi100000

分析:这道题是扫描线的进阶,不知道扫描线的可以参考我之前写的博客:线段树之扫描线

这道题目需要输出的是重叠部分的面积,那要怎么操作呢?

线段树结点我们需要保存的是覆盖一次的长度 l e n 1 len1 len1、覆盖多次的长度 l e n 2 len2 len2 以及该区间被完全覆盖的次数 c n t ​ cnt​ cnt

维护覆盖一次的长度就是扫描线的基本操作,不再赘述。

主要说说覆盖多次的长度,分类谈论:

① 如果当前区间被完全覆盖的次数 ≥ 2 \ge 2 2,那么整个区间都是满足条件的,此时 l e n 2 = r − l + 1 len2 = r-l+1 len2=rl+1

② 如果当前区间被完全覆盖的次数为 1 1 1,那么只需要把两个子区间覆盖一次的长度相加就可以了,为什么呢?因为本题我们不需要进行下传的操作,所以上层区间被覆盖了不会影响到下层的区间,因此在计算当前层的时候,虽然两个子区间的范围合起来跟自身是一样的,但是他们被覆盖的长度是独立开来的,如果当前层完全覆盖了一层,那么两个子区间覆盖一次的长度实际上就是覆盖了两次的长度!此时 l e n 2 = l e f t . l e n 1 + r i g h t . l e n 1 len2 = left.len1+right.len1 len2=left.len1+right.len1

③ 如果当前区间没有被完全覆盖,那么只能寄希望于两个子区间了,此时 l e n 2 = l e f t . l e n 2 + r i g h t . l e n 2 len2 = left.len2+right.len2 len2=left.len2+right.len2

其余的部分就是跟普通的扫描线一样了,详见代码。

Notice:这题实在是太毒了,在控制精度的时候还是用 s c a n f scanf scanf p r i n t f printf printf 吧,这题用 c i n cin cin c o u t cout cout 会疯狂WA的,原因是在某些环境下某些数值比如 2.500 2.500 2.500 的实际存储值不一样使用 C++ 的 s e t p r e c i s i o n setprecision setprecision 可能会有风险,以后需要控制精度的还是用 p r i n t f printf printf 吧!

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(线段树+区间合并)

题意:有 N ​ N​ N 个排成一排连通的村庄以及 M ​ M​ M 个事件,有三种不同的事件:(1) D   x ​ D~x​ D x,摧毁第 x ​ x​ x 个村庄;(2) Q   x ​ Q~x​ Q x,查询第 x ​ x​ x 个村庄直接与间接连接的村庄数量;(3) R ​ R​ R,恢复上一次被摧毁的村庄

范围: N , M ≤ 50000 N, M \le 50000 N,M50000

分析:经典的线段树区间合并应用,如果还没有学过的同学可以看看我的博客,介绍了一下基本的区间合并:线段树之区间合并

知道怎么利用区间合并求得某个村庄 x x x 直接和间接连接的村庄数量后,其他的问题就比较好解决了。(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(线段树+扫描线)

题意:亚特兰蒂斯中有 N N N 张地图,每张地图左上 ( x 1 , y 1 ) (x_1, y_1) (x1,y1) 和右下 ( x 2 , y 2 ) ​ (x_2, y_2)​ (x2,y2) 两个点描述了一个矩形区域,现在问这些区域的面积并是多少。

范围: 1 ≤ N ≤ 100   ,   0 ≤ x 1 < x 2 ≤ 100000   ,   0 ≤ y 1 < y 2 ≤ 100000 1 \le N \le 100~,~0 \le x_1 < x_2 \le 100000~,~0 \le y_1 < y_2 \le 100000 1N100 , 0x1<x2100000 , 0y1<y2100000

分析:经典的线段树扫描线题目了,不懂扫描线的翻我上面的链接。为什么这道题是 m e d i u m ​ medium​ medium 呢,因为但凡涉及到浮点数总是会有点莫名其妙的错误,恶心!

详见代码。

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(离散化+暴力)

题意:给一面 W ∗ H W*H WH 的墙,按顺序在上面绘制 N N N 个带有各种颜色的矩形图案,用左上角和右下角的坐标 ( x i , y i ) ​ (x_i, y_i)​ (xi,yi) 来确定矩形的位置,矩形可能会重叠,颜色会被覆盖,现在问全部绘制结束后各种颜色的矩形面积是多少?

范围: 1 ≤ W , H ≤ 10000   ,   1 ≤ N ≤ 100   ,   0 ≤ x i ≤ W   ,   0 ≤ y i ≤ H ​ 1 \le W, H \le 10000~,~1 \le N \le 100~,~0 \le x_i \le W~, ~0 \le y_i \le H​ 1W,H10000 , 1N100 , 0xiW , 0yiH

分析:本来想着这题该不会要用二维线段树来做吧,确实是可以做的,但是在网上发现了更好的解法,比二维线段书又臭又长的代码好多了,用的是离散化+暴力的解法。

由于范围并不大,所以可以直接离散化后进行模拟。

g [ i ] [ j ] g[i][j] g[i][j] 表示 x [ i ] ∼ x [ i + 1 ] x[i]\sim x[i+1] x[i]x[i+1] y [ i ] ∼ y [ i + 1 ] y[i]\sim y[i+1] y[i]y[i+1] 这个区域中的颜色。

按照输入的顺序给 g ​ g​ g 数组赋值完之后重新统计数组中的颜色面积即可。

详见代码。

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?(线段树+优化)

题意:有 N ​ N​ N 艘战舰,每个战舰有自己的初始耐力值 E i ​ E_i​ Ei,有 M ​ M​ M 个操作,操作共两种:(1) 0   X   Y ​ 0~X~Y​ 0 X Y,表示降低区间 [ X , Y ] ​ [X, Y]​ [X,Y] 之间战舰的耐力值, E i → E i ​ E_i \rightarrow \sqrt{E_i}​ EiEi ;(2) 1   X   Y ​ 1~X~Y​ 1 X Y,表示查询区间 [ X , Y ] ​ [X, Y]​ [X,Y] 之间战舰的耐力值总和。

范围: 1 ≤ N ≤ 100000   ,   ∑ E i ≤ 2 63   ,   1 ≤ M ≤ 100000 1 \le N \le 100000~,~\sum E_i \le 2^{63}~,~1 \le M \le 100000 1N100000 , Ei263 , 1M100000

分析:看起来就是简单的线段树区间修改以及区间查询,但是直接上是会超时的,需要加一个优化:当子区间的耐力值为 1 1 1 的时候就不需要更新了。在这道题中,这是个有效的优化,因为对于一个整数只需要开根号几次就会变成 1 1 1,不需要继续向下进行更新。

详见代码。

Notice:注意开 long long;可能会出现 X > Y ​ X>Y​ X>Y 的情况,需要处理一下。

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(线段树+区间合并)

题意:给两个序列 s 1 s1 s1 s 2 s2 s2,有 Q Q Q 个操作,操作共两种:(1) 1   a   i   c 1~a~i~c 1 a i c,表示将第 a a a 个串的第 i i i 位字符改成 c c c;(2) 2   i 2~i 2 i,表示询问从第 i ​ i​ i 位开始两个串匹配的长度。

范围: ∣ s 1 ∣ , ∣ s 2 ∣ ≤ 1000000   ,   Q ≤ 100000 |s_1|,|s_2| \le 1000000~,~Q \le 100000 s1,s21000000 , Q100000

分析:问题实际上可以转化为 01 ​ 01​ 01 串上单点修改以及从某位开始连续 1 ​ 1​ 1 的长度。单点修改就不说了,连续 1 ​ 1​ 1 的长度可以使用区间合并的方式来求,总体代码跟 1006 ​ 1006​ 1006 差不多,需要改一点地方。

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(双线段树+离散化)

题意:平面上有 N N N 个位置不同的点 ( x i , y i ) (x_i, y_i) (xi,yi),玩家 A A A 选择一条穿过某些点(一个或多个)的垂直线,玩家 B B B 再选择一条穿过一个点(同时被垂直线穿过)的水平线,这样两条线把平面划分成四个象限,一、三象限中点的数量就是 A A A 的得分,二、四象限中的点的数量则是 B B B 的得分,线上的点忽略。假设 B B B 一定会选择当前局面下的最优解,问 A A A 能得到的最大分数以及在该情况下 B B B 可能的得分。

范围: 1 < N < 200000 1 < N < 200000 1<N<200000 x i x_i xi y i y_i yi 是整数

分析: 细节很多,想了挺久,写了更久,还好一发就过了。

按照题目的意思我们可以发现我们只需要求得以每个点作为坐标原点时双方的答案,得到 A ​ A​ A 能获得的最优解以及此时 B ​ B​ B 的所有可行解。

先手只能选 X X X 轴,后手必定会选最优解,而我们要在后手使用最优解的情况下让自己答案尽可能大。

我们将所有点的 X X X 坐标进行分组,同一 X X X 坐标下可能会有多个点,组数不会超过 N N N

同一组中 B B B 的最优解可能有多个(真的吗?),因此我们要求每一组中 B B B 所有最优解中本方的最小值 m i n V minV minV,那么 A A A 的最优解就是所有组中最大的 m i n V minV minV

那么现在的问题就转化成以点 i i i 作为坐标原点,如何快速统计双方的分数?我是这样做的,可能有点麻烦。

将所有的点按照 x x x 坐标从小到大排序, x x x 相同则以 y y y 坐标从大到小排序,并且将所有点的 y y y 坐标进行离散化, 那么此时离散化后的 y y y 坐标不会超过 N ​ N​ N 个。

以离散化后的 y y y 坐标创建两个线段树,分别代表左侧的线段树以及右侧的线段树,线段树上的点 i i i 代表 y y y 坐标为 l i s a n [ i ] lisan[i] lisan[i] 的点的个数, l i s a n [ i ] lisan[i] lisan[i] 表示离散化后 y y y 轴上第 i i i 个点的真实 y y y 坐标。

一开始把所有的点都加入右侧的线段树,随着我们扫描每组点,将点逐步加入到左侧的线段树。

那么对于每个点,我们可以利用左侧线段树的左区间+右侧线段树的右区间得到自身可以得到的分数,对手则是左侧线段树的右区间+右侧线段树的左区间,即每个人都进行两次区间查询!

这样我们就可以求出双方的分数,当对手遇到更优解时重置自身在这组点中的最小值,遇到相同最优解时则更新最小值。

当处理完一组点之后,如果这一组的最小值 m i n V ​ minV​ minV 比答案 a n s ​ ans​ ans 大,那么更新答案,将 s e t ​ set​ set 清空,加入 B ​ B​ B 的可行解;如果跟答案相同,则把对方的当前可行解加入 s e t ​ set​ set

总体时间复杂度 O ( n l g n ) ​ O(nlgn)​ O(nlgn),详见代码。

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(线段树+分情况处理)

题意:有一个空集合 S S S,有 T T T 个操作,操作共两种:(1) B   X B~X B X,表示将 X X X 加入集合 S S S;(2) A   Y A~Y A Y,表示查询集合 S S S m o d   Y ​ mod~Y​ mod Y 的最大值。

范围: 1 ≤ T ≤ 40000   ,   1 ≤ X , Y ≤ 500000 ​ 1 \le T \le 40000~,~1 \le X, Y \le 500000​ 1T40000 , 1X,Y500000

分析:这题不好想,确实是好题!

如果操作(2)要查询的是集合中的最大值,那就是水题了,现在需要 m o d mod mod,那该怎么办呢?

这里采取分组的办法,将集合中的元素按照 Y Y Y 划分成一个一个子区间,比如 [ 0 , Y − 1 ] , [ Y , 2 Y − 1 ] . . . [0, Y-1],[Y, 2Y-1]... [0,Y1],[Y,2Y1]...,那么我们要求 S S S m o d   Y mod~Y mod Y 的最大值,相当于我们需要在这些子区间中进行区间查询,找到每个子区间的最小值,然后再取其中的最大值。那么这样本题就可以转化成为线段树单点修改与区间查询的问题了。

但是还有一个问题,如果就这样直接上线段树肯定会超时的,因为上面的想法是每个查询对集合进行分组然后再对每个分组进行区间查询,如果 Y = 1 Y = 1 Y=1 的话,区间查询就退化成单点查询,此时我们需要对 S . s i z e S.size S.size 个元素进行单点查询,铁定超时!

所以我们需要对 Y Y Y 的值进行判断,当 Y Y Y 比较小的时候我们可以直接暴力遍历集合 S S S 来找到最优解,当 Y Y Y 比较大的时候线段树的优势就出来了,在本题中 Y ≤ 5000 Y \le 5000 Y5000 使用暴力,其他情况使用线段树就可以了。

详见代码。

参考自

POJ 3145Harmony Forever(线段树更新+分情况处理数据+区间极值查询+好题)

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)

题意:给一个长度为 N N N 的序列 A 1 . . . A N A_1...A_N A1...AN, 还有 Q Q Q 个询问,每个询问 i , j i, j i,j 表示查询区间 [ i , j ] [i, j] [i,j] 中不重复数字之和。

范围: 1 ≤ N ≤ 30000   ,   0 ≤ A i ≤ 1000000000   ,   1 ≤ Q ≤ 100000 ​ 1 \le N \le 30000~, ~0 \le A_i \le 1000000000~,~ 1 \le Q \le 100000​ 1N30000 , 0Ai1000000000 , 1Q100000

分析:线段树离线处理经典题了,需要好好掌握。

问题的难点就在于不重复,对于每个询问区间中的数字我们不好通过线段树结点的信息来得知这个区间中是否重复过了,那么我们就可以对查询的处理顺序做文章。

我们先把所有的查询区间离线保存下来,按照区间的右边界进行排序。从左到右遍历这个序列,记录下该数字 A i A_i Ai 最新的出现位置,新位置的权值置为 A i A_i Ai,旧位置的权值置为 0 0 0,由于数字值域范围很大,所以需要使用 m a p map map 来记录,当处理到了某个区间的右边界,那么我们就可以使用区间查询得到该区间中不重复数字之和。由于区间是按照右边界进行排序的,所以将旧位置权值置为 0 0 0 不会对后面区间答案的正确性造成影响。

这样我们就可以得到所有区间的答案,处理完之后我们再按照查询的输入顺序重新排序,输出答案即可。

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)

题意:某个大学有个 2 ​ 2​ 2 个校区,此大学有 N ​ N​ N 个运动员,这 n ​ n​ n 个运动员在每个校区都挑选了 M ​ M​ M 个拉拉队。现在每个校区(A/B)中,这 M ∗ N ​ M*N​ MN 个拉拉队按照登记顺序说出自己支持的运动员编号 v [ i ] ​ v[i]​ v[i] 和自己想排在那个位置 w [ i ] ​ w[i]​ w[i],排成一列。如果冲突则按照先来后到的顺序依次往后排。求按照A/B的两个拉拉队员站的位置,用他们支持的运动员编号形成的两个序列的最长公共子序列。

范围: 1 ≤ N ≤ 10000   ,   1 ≤ M ≤ 10   ,   1 ≤ v [ i ] ≤ N   ,   1 ≤ w [ i ] ≤ M ∗ N ​ 1 \le N \le 10000~,~1 \le M \le 10~,~1 \le v[i] \le N~,~1 \le w[i] \le M*N​ 1N10000 , 1M10 , 1v[i]N , 1w[i]MN

分析:有两个子问题需要处理,第一个就是需要得到两个序列,他们之间会出现冲突,他们的位置可以使用二分来解决,这个好办。困难的是第二个问题,求两个序列的最长公共子序列,当然直接上 L C S LCS LCS 算法肯定是不行的,其实这种问题有专门的名字:稀疏序列匹配。稀疏序列匹配的问题可以通过处理转化成区间最值问题,然后使用线段树/树状数组等数据结构来解决,但是这道题目时间卡得比较紧,所以线段树比较难写!不过这题有更有意思的解法,就是将 L C S LCS LCS 问题转化成 L I S ​ LIS​ LIS 问题。这个思维的转换确实可以学习一下。

具体怎么做呢?先得到我们需要处理的两个序列 A A A B B B,我们可以得到 B B B 中每个元素在 A A A 中所有匹配的位置,将这些位置倒叙保存起来,然后将所有元素倒叙保存的位置拼接起来,跑 L I S LIS LIS 即可!

没懂?先把问题简化一下,假设序列 A ​ A​ A B ​ B​ B 内部中的元素不重复,那么怎么求 L C S ​ LCS​ LCS 呢?因为 L C S ​ LCS​ LCS 不要求子序列连续,只需要保证序号要递增,那么我们是不是就可以求出 B ​ B​ B 中元素在 A ​ A​ A 中匹配的位置,形成序列,而我们要求 L C S ​ LCS​ LCS,就要求这个序列中最长的递增子序列,即 L I S ​ LIS​ LIS

现在再考虑重复的问题, B B B 中每个元素可能对应 A A A 中多个位置,如果直接跑 L I S LIS LIS 会导致 B B B 中一个元素跟 A A A 中元素形成多次匹配,导致答案不正确,那么怎么消除这个影响呢?就是把 B B B 中每个元素在 A ​ A​ A 中的匹配序列进行倒置,那么就保证一个元素自身的匹配序列是递减的,不会形成多次匹配的情况。

所以我们需要把所有元素的匹配序列进行倒叙之后拼接,这样跑 L I S ​ LIS​ LIS 就是我们要求的 L C S ​ LCS​ LCS

详见代码。

参考自

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 舞蹈链)

题意:房间里面有 N N N 盏灯和 M ​ M​ M 个开关,一盏灯可以由多个开关控制,一个开关最多只能控制两盏灯,现在问是否能够通过打开某些开关和关闭某些开关使所有的灯亮起来。

范围: 1 ≤ N , M ≤ 500 1 \le N, M \le 500 1N,M500

分析:经典的舞蹈链问题,利用交叉十字循环双向链表实现的精确覆盖/重复覆盖问题的算法,舞蹈链我这里就不说了吧,篇幅实在太长了,现在不会也没关系,这种数据结构应该我们后续的训练也会涉及到。

有兴趣的同学 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】感谢观看

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值