JXOI 2017 简要题解

「JXOI2017」数列

题意

九条可怜手上有一个长度为 \(n\) 的整数数列 \(r_i\) ,她现在想要构造一个长度为 \(n\) 的,满足如下条件的整数数列 \(A\) :

  • \(1\leq A_i \leq r_i\)
  • 对于任意 \(3 \leq i \leq n\) ,令 \(R\)\(A_1\)\(A_{i-2}\) 中大于等于 \(A_{i-1}\) 的最小值, \(L\)\(A_1\)\(A_{i-2}\) 中小于等于 \(A_{i-1}\) 的最大值。\(A_i\) 必须满足 \(L \leq A_i \leq R\)。如果不存在大于等于 \(A_{i-1}\) 的,那 么 \(R = +\infty\) ;如果不存在小于等于 \(A_{i-1}\) 的,那么 \(L = −\infty\)

现在可怜想要知道共有多少不同的数列满足这个条件。两个数列 \(A\)\(B\) 是不同的当且仅当至少存在一个位置 \(i\) 满足 \(A_i \neq B_i\)

\(n \le 50, A_i \le 150\)

题解

首先不难发现 \(L_i\) 是单调不降,\(R_i\) 是单调不升的,也就是说 \([L_i, R_i]\) 是不断收缩的。

然后发现 \(A_i\) 会在一定会充当 \(L_{i + 1}\)\(R_{i + 1}\) ,注意 \(A_i \in \{L_i, R_i\}\) 的时候,会同时充当 \(L_{i + 1}, R_{i + 1}\)

不难得出一个 \(dp\)\(dp[i][l][r]\) 为到第 \(i\) 个点,下界为 \(l\) 上界为 \(r\) 的方案数。

至于边界,一开始暴力枚举前面两个数取值就行了,讨论三种情况就不赘述了。

然后转移的话我们对于 \([l, r]\) 这段区间只需要枚举 \([l, r]\) 之中的数来转移即可。

具体来说我们假设把 \([l, r]\) 这段区间分成 \([l, p]\)\([p, r]\) 这两种不同的区间,直接更新 \(dp[i + 1][l][p]\)\(dp[i + 1][p][r]\) 就行了,但是会发现 \(p\) 会被更新两次,那么在 \(dp[i + 1][p][p]\) 减去即可。

然后注意当 \(p\) 取到 \(l~or~r\) 的时候,转移的就是 \(dp[i + 1][p][p]\) 了。

最后复杂度就是 \(O(n \max^3(A_i))\) ,常数很小可以跑过。

好像看到了孔爷是 \(O(n \max^2(A_i))\)做法 ,恐怖如斯。。。

其实似乎是把一系列相同转移的转移到一起,用填表法转移就行了。

代码

懒得特判 \(n=1,2\) 的情况了,反正数据中没有。。

#include <bits/stdc++.h>

#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << (x) << endl
#define DEBUG(...) fprintf(stderr, __VA_ARGS__)

using namespace std;

template<typename T> inline bool chkmin(T &a, T b) { return b < a ? a = b, 1 : 0; }
template<typename T> inline bool chkmax(T &a, T b) { return b > a ? a = b, 1 : 0; }

inline int read() {
    int x(0), sgn(1); char ch(getchar());
    for (; !isdigit(ch); ch = getchar()) if (ch == '-') sgn = -1;
    for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48);
    return x * sgn;
}

void File() {
#ifdef zjp_shadow
	freopen ("2273.in", "r", stdin);
	freopen ("2273.out", "w", stdout);
#endif
}

const int N = 55, M = 155, Mod = 998244353;

int n, R[N], dp[N][M][M], maxr;

inline void Update(int pos, int l, int r, int mid, int val) {
	(dp[pos][l][mid] += val) %= Mod;
	(dp[pos][mid][r] += val) %= Mod;
	(dp[pos][mid][mid] += Mod - val) %= Mod;
}

int main () {

	File();

	n = read(); For (i, 1, n) R[i] = read();

	maxr = *max_element(R + 1, R + n + 1) + 1;

	For (i, 1, R[1]) For (j, 1, R[2]) {
		if (i == j) Update(3, i, j, i, 1);
		if (i > j) Update(3, 0, i, j, 1);
		if (i < j) Update(3, i, maxr, j, 1);
	}

	For (i, 3, n - 1) For (l, 0, maxr) For (r, l, maxr) if (dp[i][l][r]) 
		For (cur, l, min(r, R[i]))
			if (cur == l || cur == r) Update(i + 1, cur, cur, cur, dp[i][l][r]);
			else Update(i + 1, l, r, cur, dp[i][l][r]);

	int ans = 0;
	For (l, 0, maxr) For (r, 0, maxr)
		ans = (ans + 1ll * dp[n][l][r] * max(0, (min(r, R[n]) - max(l, 1) + 1))) % Mod;
	printf ("%d\n", ans);

	return 0;

}

「JXOI2017」加法

题意

有一个长度为 \(n\) 的正整数序列 \(A\)

一共有 \(m\) 个区间 \([l_i, r_i]\) 和两个正整数 \(a, k\) 。从这 \(m\) 个区间里选出恰好 \(k\) 个区间,并对每个区间执行一次区间加 \(a\) 的操作。(每个区间最多只能选择一次。)

最后最大化 \(\min A_i\)

\(n, m \le 2 \times 10^5\)

题解

最大化最小值,基本二分答案跑不掉了。

二分 \(\min A_i\) ,也就是使得 \(\forall A_i \ge mid\)

然后考虑如何 \(check\) 。可以贪心,考虑把 \(m\) 个区间放到对应的左端点处。

从左往右依次考虑每个点,贪心选择左端点在之前,右端点尽量远的区间,这样肯定最优。

然后不断加 \(a\) 使得当且 \(A_i \ge mid\) 即可,如果区间不够那么就不可行。

至于具体实现,用堆存储右端点最靠右的点,区间加可以直接差分掉就行了。

复杂度是 \(O((n + m) \log m \log A_i)\) 的,常数不大,跑得很快。

代码

#include <bits/stdc++.h>
 
#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << (x) << endl
#define DEBUG(...) fprintf(stderr, __VA_ARGS__)
 
using namespace std;
 
template<typename T> inline bool chkmin(T &a, T b) {return b < a ? a = b, 1 : 0;}
template<typename T> inline bool chkmax(T &a, T b) {return b > a ? a = b, 1 : 0;}
 
inline int read() {
    int x(0), sgn(1); char ch(getchar());
    for (; !isdigit(ch); ch = getchar()) if (ch == '-') sgn = -1;
    for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48);
    return x * sgn;
}
 
inline void File() {
#ifdef zjp_shadow
    freopen ("2274.in", "r", stdin);
    freopen ("2274.out", "w", stdout);
#endif
}
 
const int N = 2e5 + 1e3, inf = 0x7f7f7f7f;
 
int n, m, k, a, val[N], tag[N];
 
vector<int> seg[N];
 
priority_queue<int> P;
 
inline bool check(int limit) {
    int cnt = 0, add = 0, now, here;
    while (!P.empty()) P.pop();
    For (i, 1, n) tag[i] = 0;
    For (i, 1, n) {
        add += tag[i];
        here = val[i] + add;
        for(int j : seg[i]) P.push(j);
             
        while (here < limit) {
            if (++ cnt > k || !(bool)P.size()) return false;
            now = P.top(); P.pop();
            if (now < i) return false;
            tag[now + 1] -= a; here += a; add += a;
        }
    }
    return true;
}
 
int main() {
 
    File();
 
    int cases = read();
    while (cases--) {
        int minv = inf;
        n = read(); m = read(); k = read(); a = read();
        For (i, 1, n) 
            chkmin (minv, val[i] = read());         
         
        For (i, 1, m) {
            int pos = read();
            seg[pos].push_back(read());
        }
         
        int l = 1, r = minv + a * k, ans = 0;
        while (l <= r) {
            int mid = (l + r) >> 1;
            if (check(mid)) l = mid + 1, ans = mid;
            else r = mid - 1;
        }
        printf ("%d\n", ans);
        For (i, 1, n) seg[i].clear();
    }
 
    return 0;
 
}

「JXOI2017」颜色

题意

有一个长度为 \(n\) 的颜色序列 \(A_i\) ,选择一些颜色把这些颜色的所有位置都删去。

删除颜色 \(i\) 可以定义为把所有满足 \(A_j = i\) 的位置 \(j\) 都从序列中删去。

想要知道有多少种删去颜色的方案使得最后剩下来的序列非空且连续。

\(n \le 3 \times 10^5\)

题解

认真读题,注意是 一起 删除。

考虑删除方案显然不太好算的,可以考虑最后剩下的序列。

我们记 \(\min_i\)\(i\) 这种颜色最早出现的位置,\(\max_i\)\(i\) 这种颜色最晚出现的位置。

枚举最后剩下序列的右端点 \(r\) ,我们只需要查找左端点 \(l\) 在哪里合法。

  1. 对于一种颜色 \(k\) ,如果存在 \(\max_k > r\) ,那么这种颜色 \(k\) 不能存在于 \([l, r]\) 之中。我们只需要找出 \(\max_{j < r} j\) 满足 \(\max_{A_j} > r\) ,那么 \(l \in (j, r]\)
  2. 对于一种颜色 \(k\) ,如果存在 \(\max_k \le r\) ,那么 \(l \notin (\min_k,\max_k]\)

不难发现只要满足这两个限制,外面的颜色可以一起删完且不会影响到中间这段区间的点。

第一个保证右端点向右合法,第二个保证左端点向左合法。

如何维护呢?

  1. 对于第一个求 \(j\) 直接维护一个 \(i\) 递增 \(\max_{A_i}\) 递减的单调栈就行了。
  2. 第二个直接到 \(max_{A_i}\) 的时候,在线段树上打一个 $ (\min_k,\max_k]$ 区间不可行的标记就行。

然后每次就直接线段树上查找可行节点个数就能轻松做完了。

代码

#include <bits/stdc++.h>
#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
using namespace std;

inline bool chkmin(int &a, int b) {return b < a ? a = b, 1 : 0;}
inline bool chkmax(int &a, int b) {return b > a ? a = b, 1 : 0;}

inline int read() {
    int x = 0, fh = 1; char ch = getchar();
    for (; !isdigit(ch); ch = getchar()) if (ch == '-') fh = -1;
    for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
    return x * fh;
}

void File() {
#ifdef zjp_shadow
	freopen ("2275.in", "r", stdin);
	freopen ("2275.out", "w", stdout);
#endif
}

const int N = 3e5 + 1e3;
struct Stack {
	int pos[N], val[N], top;

	void Clear() { top = 0; }

	inline void Push(int p, int v) { pos[++ top] = p; val[top] = v; }

	inline int Max_Pos() { return pos[top]; } 

	inline void Pop(int p) { while (top && val[top] <= p) -- top; }
} S;

#define lson o << 1, l, mid
#define rson o << 1 | 1, mid + 1, r
struct Segment_Tree {
	int sumv[N << 2]; bitset<(N << 2)> Tag;

	Segment_Tree () { Tag.reset(); }

	inline void push_down(int o) {
		if (!Tag[o]) return ; sumv[o << 1] = sumv[o << 1 | 1] = 0; Tag[o << 1] = Tag[o << 1 | 1] = true; Tag[o] = false;
	}

	inline void push_up(int o) { sumv[o] = sumv[o << 1] + sumv[o << 1 | 1]; }

	void Build(int o, int l, int r) {
		Tag[o] = false; sumv[o] = r - l + 1; if (l == r) return ;
		int mid = (l + r) >> 1; Build(lson); Build(rson);
	}

	void Update(int o, int l, int r, int ul, int ur) {
		if (!sumv[o] || ul > ur) return ; 
		if (ul <= l && r <= ur) { sumv[o] = 0; Tag[o] = true; return ; }
		push_down(o); int mid = (l + r) >> 1; 
		if (ul <= mid) Update(lson, ul, ur); if (ur > mid) Update(rson, ul, ur); push_up(o);
	}

	int Query(int o, int l, int r, int ql, int qr) {
		if (!sumv[o] || ql > qr) return 0; 
		if (ql <= l && r <= qr) return sumv[o];
		int res = 0, mid = (l + r) >> 1; push_down(o);
		if (ql <= mid) res += Query(lson, ql, qr); if (qr > mid) res += Query(rson, ql, qr); return res; push_up(o);
	}
} T;

const int inf = 0x7f7f7f7f;
int n, Col[N], Min[N], Max[N];

int main () {
	File();
	int cases = read();
	while (cases --) {
		n = read(); 
		For (i, 1, n) Col[i] = read(), Min[Col[i]] = inf, Max[Col[i]] = -inf;
		For (i, 1, n) chkmax(Max[Col[i]], i), chkmin(Min[Col[i]], i);

		T.Build(1, 1, n); S.Clear();
		long long ans = 0;
		For (i, 1, n) {
			S.Push(i, Max[Col[i]]); S.Pop(i); int Pos = S.Max_Pos();
			if (i == Max[Col[i]]) T.Update(1, 1, n, Min[Col[i]] + 1, Max[Col[i]]);
			ans += T.Query(1, 1, n, Pos + 1, i);
		}
		printf ("%lld\n", ans);
	}
    return 0;
}

总结

总的来说,这套吉老师出的题水平很高,做起来十分的舒服。

据说现场有大样例,但是没人看到。。。

如果给 \(5h\) 就很舒服,因为三题都需要对拍比较好。。。但是据说现场只有 \(3.5h\) 喵喵喵?

第一题考察了比较基础的计数 \(dp\) 和对性质的观察。

第二题考察了二分答案的基本应用然后转化为贪心选择区间覆盖的经典问题,用堆维护即可。

第三题依旧考察了对于性质的观察以及用数据结构维护可行节点的经典操作。

整体来说,是套好题。

posted @ 2018-11-15 16:02  zjp_shadow  阅读(353)  评论(4编辑  收藏  举报