【补题笔记】Educational Codeforces Round 130 (CF1697)

A - Parkway Walk

题目链接

可以选择在起点的椅子上一直休息,直到自己的体力足够走完接下来的全程后,一口气从起点走到终点。这样可以避免中间处理走不到下一个椅子就没有体力的情况,同时不难想到,这个做法一定是最优的,因为我们只恢复了必要的体力。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;

int n, m;
int a[105];

void main2() {
    
    
	int sum = 0;
	cin >> n >> m;
	for (int i = 1; i <= n; ++i) {
    
    
		cin >> a[i];
		sum += a[i];
	}
	cout << max(0, sum - m) << '\n';
}

int main() {
    
    
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	LL _ = 1;
	cin >> _;
	while (_--) main2();
	return 0;
}

B - Promo

题目链接

将所有物品按照价钱从小到大排序,至少 x x x件可以触发优惠,每次优惠只能减省最便宜的 y y y件。显然,我们要选择购买最贵的 x x x件,这样才能保证减省的 y y y件是所有可能的情况中价值最高的 y y y件。

排序后用前缀和维护一下,然后选取第 n − x + 1 n-x+1 nx+1 n − x + y n-x+y nx+y个货物的价值之和即为答案。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;

LL n, m;
LL a[200005], pre[200005];

void main2() {
    
    
	cin >> n >> m;
	pre[0] = 0;
	for (int i = 1; i <= n; ++i) {
    
    
		cin >> a[i];
	}
	sort(a + 1, a + n + 1);
	for (int i = 1; i <= n; ++i) {
    
    
		pre[i] = pre[i - 1] + a[i];
	}
	for (int i = 1; i <= m; ++i) {
    
    
		int x, y;
		cin >> x >> y;
		cout << pre[n - x + y] - pre[n - x] << '\n';
	}
}

int main() {
    
    
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	LL _ = 1;
//	cin >> _;
	while (_--) main2();
	return 0;
}

C - awoo’s Favorite Problem

题目链接

注意到两种操作并不会改变 a , b , c a,b,c a,b,c三个字母的数量,所以如果两个串中 a , b , c a,b,c a,b,c的数量不同,直接输出NO。

第一个操作实质上是字母 a a a借助字母 b b b向后移动,第二个操作实质上是字母 c c c借助字母 b b b向前移动。也就是说,一旦移动后 a , c a,c a,c相遇,就不会再发生移动了。换言之,两个字符串将所有的字母 b b b去掉,那么得到的两个字符串应当是相同的。如果不同,则直接输出NO。

再考虑移动的合法性:对于从 s s s串到 t t t串的过程,看每一个对应的 a a a有没有往前跑的,有就是NO;再看每一个对应的 c c c有没有往后跑的,有就是NO。

若以上情况都没有输出NO,则直接输出YES。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;

int n;
string s, t; 
int cnt1[5], cnt2[5];
vector<int> as, at, cs, ct;

void main2() {
    
    
	cin >> n;
	cin >> s >> t;
	string ts, tt;
	as.clear(); cs.clear(); at.clear(); ct.clear();
	for (int i = 0; i < 3; ++i) {
    
    
		cnt1[i] = cnt2[i] = 0;
	}
	for (int i = 0; i < n; ++i) {
    
    
		cnt1[s[i] - 'a']++;
		cnt2[t[i] - 'a']++;
		if (s[i] == 'a') as.push_back(i);
		if (s[i] == 'c') cs.push_back(i);
		if (t[i] == 'a') at.push_back(i);
		if (t[i] == 'c') ct.push_back(i);
		if (s[i] != 'b') ts += s.substr(i, 1);
		if (t[i] != 'b') tt += t.substr(i, 1);
	}
	if (ts != tt) {
    
    
		cout << "NO\n";
		return;
	}
	for (int i = 0; i < 3; ++i) {
    
    
		if (cnt1[i] != cnt2[i]) {
    
    
			cout << "NO\n";
			return;
		}
	}
	for (int i = 0; i < as.size(); ++i) {
    
    
		if (as[i] > at[i]) {
    
    
			cout << "NO\n";
			return;
		}
	}
	for (int i = 0; i < cs.size(); ++i) {
    
    
		if (cs[i] < ct[i]) {
    
    
			cout << "NO\n";
			return;
		}
	}
	cout << "YES\n";
}

int main() {
    
    
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	LL _ = 1;
	cin >> _;
	while (_--) main2();
	return 0;
}

D - Guess The String

题目链接

交互题的切入点,就是他所给定的查询次数限制。

我们发现他给定的字符串长度只有 1000 1000 1000,而查询是什么字母的次数不超过 26 26 26,说明只允许每一种字母只查询一次。
又有一种操作,是每个区间查询字母种类数,可以查询 6000 6000 6000次。刚看到这个条件的时候,并不知道这个 6000 6000 6000次是从哪里来的。

第一眼我们可以想到,如果每个字母只查询一次,那么我们可以让连续的相同的字母拼接成一个序列,来减少我们的最基础的查询次数,因为这样我们可以从每一个位置都查询,变成只查询每一段的第一个字母,因为这一段里字母都跟第一个字母一样,所以查询这一段里面后面的字母也没有意义。

所以有了一个初步的思路,就是先将字符串分成一些个连续相同字符的段,但是先不考虑这个段的字母是哪个字母。

怎么实现呢?既然先不考虑是什么字母,那就用不上第一个操作了,只能用操作二。我们可以利用字符种数的变化来确认段的结尾——当我们看连续的一段的时候,这个区间的字符种类数应当一直是 1 1 1,当字符种类数变成 2 2 2时,说明到这一位字符就变了,前一段已经在上一位结束了。
用这个思路,可以用 1000 1000 1000次第二种查询来将字符串分开成段。

然后考虑如何获得每一个段是什么字符。

既然我们只能查询 26 26 26次字符种类,说明我们每一种字符我们只能查询 1 1 1次。再一次遇到查询过的字符,就只能通过之前出现的那个字符来对这一位是否是那个字符进行判断。

我们还有 5000 5000 5000次第二种询问可以辅助我们完成这个任务。先不考虑次数问题,单看这个问题,利用第二种查询该如何操作?

假设我们验证第 6 6 6位是什么字母。假设上一次 a a a出现是在第 2 2 2位。中间还有上一次在第 4 4 4位出现的字符 b b b和上一次在第 5 5 5位出现的字符 c c c。也就是说,我们维护一个上一次出现的字母的位置的数组,大概会长成这个样子:

{ [ a , 2 ] , [ b , 4 ] , [ c , 5 ] } \{ [a,2],[b,4],[c,5] \} {[a,2],[b,4],[c,5]}

这时我们可以断定,从第 2 2 2位到第 5 5 5位,一共就这 3 3 3种字母。如果第 3 3 3位不是 a , b , c a,b,c a,b,c中的一种,而是其他的字母,那么这个字母应当也会出现作为某一个字母的最后一次出现,而出现在数组里,说明第 3 3 3位一定是 b , c b,c b,c中的一个数。

那么就利用操作 3 3 3,来看每一个字母和第 6 6 6位之间有几种字母。假设现在轮到与 a a a比较了,如果 [ 2 , 6 ] [2,6] [2,6]之间有 4 4 4种字母,那么第 6 6 6位一定是 a , b , c a,b,c a,b,c之外的字符,直接用第一种询问读进来;如果 [ 2 , 6 ] [2,6] [2,6]之间有 3 3 3种字母,说明第 6 6 6个字母一定也是 a , b , c a,b,c a,b,c中的一个,然后接着往下判 b b b,如果发现 [ 4 , 6 ] [4,6] [4,6]只有 2 2 2种了,说明第 6 6 6位不是 a a a,而是 b , c b,c b,c中的一种;如果发现 [ 4 , 6 ] [4,6] [4,6]还是 3 3 3种,说明第 6 6 6位必然是 a a a

以此类推,就可以判断出这一位是哪个字符了。

但是这样,每一个位置至多可能会判断 26 26 26次, 1000 1000 1000位的字符串需要 26000 26000 26000次第二种查询,显然不可行。我们现在只剩下 5000 5000 5000次查询了,每一个位置至多判断 5 5 5次。很快就能想到,可能需要将 O ( n ) O(n) O(n)变为 O ( log ⁡ n ) O(\log n) O(logn)。难道是二分?

我们发现,如果这个字母是 x x x,那么这个字母在我们维护的“最后一次出现的数组”中的位置考量,判断前面的字母 y y y时,判断 y y y最后一次出现的位置到判断位,查询得到的字符种数应当与维护的数组中字符 y y y到最后一个字符的种数相同;判断后面的字母 z z z时,判断 z z z最后一次出现的位置到判断位,查询得到的字符种数应当比维护的数组中 z z z到最后一个字符的种数少。

——于是我们发现了单调性,确实可以通过二分来减少查询次数!

这样一位一位判断完,就得到最后的字符串了。

当然,如果最开始得到的段的数量不大于 26 26 26,也可以每一段都用一次第一种查询,然后直接得到答案。(不过没什么必要)

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;

int query1(int i) {
    
    
	cout << "? 1 " << i << endl;
	char c; cin >> c;
	return (int)(c - 'a');
}

int query2(int l, int r) {
    
    
	cout << "? 2 " << l << ' ' << r << endl;
	int x; cin >> x;
	return x;
}

struct seg {
    
    
	int l, r, id;
}q[1005]; 

int n, qi, si;
int s[10005], pos[32];

void output() {
    
    
	cout << "! ";
	for (int i = 1; i <= qi; ++i) {
    
    
		for (int j = q[i].l; j <= q[i].r; ++j) {
    
    
			cout << (char)(q[i].id + 'a'); 
		}
	}
	cout << endl;
}

bool check(int x, int cur) {
    
    
	int tmp = query2(pos[s[x]], cur), kd = si - x + 1;
	if (tmp > kd) return false;
	else return true;
}

void main2() {
    
    
	qi = 0;
	cin >> n;
	int st = 1;
	for (int i = 2; i <= n; ++i) {
    
    
		int tmp = query2(st, i);
		if (tmp > 1) {
    
    
			q[++qi] = {
    
    st, i - 1, -1};
			st = i;
		}
	}
	q[++qi] = {
    
    st, n, -1};
	if (qi <= 26) {
    
    
		for (int i = 1; i <= qi; ++i) {
    
    
			q[i].id = query1(q[i].l);
		}
	}
	else {
    
    
		si = 0;
		for (int i = 0; i < 26; ++i) {
    
    
			pos[i] = 0;
		}
		for (int i = 1; i <= qi; ++i) {
    
    
			int l = 1, r = si, ans = 0;
			while (l <= r) {
    
    
				int mid = (l + r) >> 1;
				if (check(mid, q[i].l)) {
    
    
					ans = max(ans, mid);
					l = mid + 1;
				}
				else {
    
    
					r = mid - 1;
				}
			}
			if (!ans) {
    
    
				int tmp = query1(q[i].l);
				pos[tmp] = q[i].l;
				s[++si] = q[i].id = tmp;
			}
			else {
    
    
				q[i].id = s[ans];
				for (int j = ans; j < si; ++j) {
    
    
					s[j] = s[j + 1];
				}
				s[si] = q[i].id;
				pos[s[si]] = q[i].l;
			}
		}
	}
	output();
}

int main() {
    
    
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	LL _ = 1;
//	cin >> _;
	while (_--) main2();
	return 0;
}

E - Coloring

题目链接

这道题目读出题目想传达的信息非常关键。注意题目中给定的两个信息,我们可以从中得到这样的性质:

对于任意一个点 u u u,如果有另外一个点 v v v和这个点颜色相同,那么这个点一定是离点 u u u的距离最近的点之一。
(假设有另外一个点 x x x u u u的距离比 v v v小,则以 u , v , x u,v,x u,v,x三个点构成的三元组不满足题目中的第二个条件)

如果有第三个点和这两个点的颜色一样呢?
根据题目中的第一个条件,我们可知,这三个点之间任意两点的距离相等。

如果有 x x x个点的颜色一样呢?
那么从这 x x x个点中,任意选出 3 3 3个点,这 3 3 3个点之间任意两点的距离相等。由此我们可以推出,颜色相同的 x x x个点之间必然是任意两点之间的距离相等。

所以,我们可以认为,将这些点分成组,每一组中的点任意两点之间的距离都要是一个定值。而且,任意两个点之间的距离必须是这两个点与其他点之间的距离的最小值。
同时,我们还可以发现,这样的组,填充颜色只有两种可能:一种是全部填成同一种颜色,一种是所有点的颜色都不一样。
(因为如果有两个点的颜色相同,由于彼此之间的距离都相等,那么第三个点也要相同,其他的点也应当相同,于是就变回所有点都是一个颜色的情况了)

遵循着这样的思路,我们现在对 n n n个点进行如下的处理:考察每个点与所有点之间的距离,记录其中的最小值。然后带着最小值重新遍历所有的点。设这个点是 u u u,如果遇到和这个点 v v v的距离是最小值的点,就从 u u u v v v连一条边。

然后,我们开始给每个点找组员。刚刚我们用这 n n n个点建立了一个不一定连通的有向图。接下来依次遍历每一个点:

  1. 如果这个点已经被前面的点当作组员,则直接跳过这个点。
  2. 从这个点出发,进行dfs,将所有能走到的点记录在一个集合中。
  3. 看看集合中是否满足任意两点之间的距离都是定值,而且是最小值。
  4. 如果不满足,那么这个点就只能自己一组;如果与自己连通的点的集合符合性质,就把这些点都标记为已被当做组员,然后存到一个set中。
  5. 根据这个点所在的小组,将这个小组的元素个数记录下来(如果是被跳过的,那么这一点所得到的的记录为 0 0 0)。

这样,我们就将 n n n个点全部分好组了。现在我们就按照小组进行颜色的讨论。一个小组能产生的颜色只有两种情况:一种是 1 1 1(全部填一种颜色),一种是小组元素个数(都填不一样的颜色)。

接下来算方案数。假设我们把 n n n个点分为了 k k k组。我们用 d p dp dp来计算这个方案数。

我们设 d p [ i ] [ j ] dp[i][j] dp[i][j]表示前 i i i个小组用了 j j j个颜色的方案数。我们遍历时只考虑小组数大于等于 1 1 1的,有一些在前面被标记了小组大小为 0 0 0的就直接跳过了。

求解时要注意的是:如果这个颜色被这个小组用了,那么这个颜色在别的小组就不能再使用了,否则会违背题目中给出的两条性质(和前面讨论的基本相同)。

接下来讨论转移方程:

设第 i i i个小组的元素个数是 a [ i ] a[i] a[i]
对于前 i i i个小组,用了 j j j个颜色的状态,我们可以从以下情况转移过来:

  1. i − 1 i-1 i1个小组,用了 j − 1 j-1 j1个颜色的状态,第 i i i个小组选择全部填充 1 1 1种颜色。
  2. i − 1 i-1 i1个小组,用了 j − a [ i ] j-a[i] ja[i]个颜色的状态(要保证 k − a [ i ] ≥ 0 k-a[i]\geq 0 ka[i]0),第 i i i个小组选择每个点都填充不同的颜色,一共填充了 a [ i ] a[i] a[i]种颜色。

分别考虑两种情况为 d p [ i ] [ j ] dp[i][j] dp[i][j]带来的贡献:

  1. 在前 i − 1 i-1 i1个小组用了 j − 1 j-1 j1个颜色的情况下,这个小组可以使用的颜色种数是 n − ( j − 1 ) = n − j + 1 n-(j-1)=n-j+1 n(j1)=nj+1种。所以这种情况为 d p [ i ] [ j ] dp[i][j] dp[i][j]带来的贡献就是: ( n − k + 1 ) d p [ i − 1 ] [ j − 1 ] (n-k+1)dp[i-1][j-1] (nk+1)dp[i1][j1]
  2. 在前 i − 1 i-1 i1个小组用了 j − a [ i ] j-a[i] ja[i]个颜色的情况下,这个小组可以使用的颜色种树是 n − ( j − a [ i ] ) = n − j + a [ i ] n-(j-a[i])=n-j+a[i] n(ja[i])=nj+a[i]种,这个小组会挑选其中的 a [ i ] a[i] a[i]种,所以这种情况为 d p [ i ] [ j ] dp[i][j] dp[i][j]带来的贡献是: ( n − j + a [ i ] a [ i ] ) × ( a [ i ] ! ) × d p [ i − 1 ] [ k − a [ i ] ] \binom{n-j+a[i]}{a[i]}\times (a[i]!)\times dp[i-1][k-a[i]] (a[i]nj+a[i])×(a[i]!)×dp[i1][ka[i]]

转移时对于 d p [ i ] [ j ] dp[i][j] dp[i][j],直接将这两种贡献加进来就好了。注意取模。

最后的答案就是全部的小组,用了 1 1 1种颜色到 n n n种颜色的方案数之和。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 105;
const LL mod = 998244353;

LL n, en = 0, cnt = 0;
LL d[N], x[N], y[N], a[N], dp[N][N], front[N], vis[N];
LL jc[N * 3], inv[N * 3];

template<class T> T power(T a, LL b) {
    
    
	T res = 1;
    for (; b; b >>= 1) {
    
    
        if (b % 2) res = (res * a) % mod;
        a = (a * a) % mod;
    }
    return res;
}

void init(LL iN) {
    
    
	jc[0] = 1;
	for (LL i = 1; i <= iN + 1; ++i) {
    
    
		jc[i] = (jc[i - 1] * i) % mod;
	}
	inv[iN + 1] = power(jc[iN + 1], mod - 2);
	for (LL i = iN; i >= 0; --i) {
    
    
		inv[i] = inv[i + 1] * (i + 1) % mod;
	} 
}

LL C(LL N, LL M) {
    
    
	return jc[N] * inv[M] % mod * inv[N - M] % mod;
}

set<int> s[N];

LL dis(int i, int j) {
    
    
	return abs(x[i] - x[j]) + abs(y[i] - y[j]);
}

struct Edge {
    
    
	LL v, next;
}e[100005];

void addEdge(int u, int v) {
    
    
	e[++en] = {
    
    v, front[u]};
	front[u] = en;
}

void dfs(int u, int rt) {
    
    
	if (s[rt].count(u) == 1) return;
	s[rt].insert(u);
	for (int i = front[u]; i; i = e[i].next) {
    
    
		int v = e[i].v;
		dfs(v, rt);
	}
}

void main2() {
    
    
	cin >> n;
	for (int i = 1; i <= n; ++i) {
    
    
		cin >> x[i] >> y[i];
		a[i] = vis[i] = 0;
		s[i].clear();
	}
	for (int i = 1; i <= n; ++i) {
    
    
		d[i] = 1e15;
		for (int j = 1; j <= n; ++j) {
    
    
			if (i == j) continue;
			d[i] = min(d[i], dis(i, j));
		}
	}
	for (int i = 1; i <= n; ++i) {
    
    
		for (int j = 1; j <= n; ++j) {
    
    
			if (i == j) continue;
			if (dis(i, j) == d[i]) addEdge(i, j);
		}
	}
	for (int i = 1; i <= n; ++i) {
    
    
		if (vis[i]) continue;
		dfs(i, i);
		int ok = 1;
		for (int x: s[i]) {
    
    
			for (int y: s[i]) {
    
    
				if (x == y) continue;
				if (dis(x, y) != d[i]) {
    
    
					ok = 0;
				}
			}
		}
		if (!ok) {
    
    
			a[i] = 1;
			vis[i] = i;
		}
		else {
    
    
			for (int x: s[i]) {
    
    
				vis[x] = i;
			}
			a[i] = s[i].size();
		}
	}
	for (int i = 1; i <= n; ++i) {
    
    
		for (int j = 1; j <= n; ++j) {
    
    
			dp[i][j] = 0;
		}
	}
	dp[0][0] = 1;
	int cnt = 0;
	for (int i = 1; i <= n; ++i) {
    
    
		if (a[i] > 0) {
    
    
			++cnt;
			for (int j = 1; j <= n; ++j) {
    
    
				dp[cnt][j] = (dp[cnt][j] + (n - j + 1) * dp[cnt - 1][j - 1]) % mod;
				if (j >= a[i] and a[i] > 1) {
    
    
					dp[cnt][j] = (dp[cnt][j] + C(n - j + a[i], a[i]) * jc[a[i]] % mod * dp[cnt - 1][j - a[i]] % mod) % mod;
				}
			}
			
		}
	}
	LL ans = 0;
	for (int i = 1; i <= n; ++i) {
    
    
		ans = (ans + dp[cnt][i]) % mod;
	}
	cout << ans;
}

int main() {
    
    
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	LL _ = 1;
	init(200);
//	cin >> _;
	while (_--) main2();
	return 0;
}

F - Too Many Constraints

题目链接

这道题目乍一眼一看没什么思路,因为贪心和DP好像都不太可行。

但是这道题目的形式,无论从题目名称还是题目内容,都是在“限制”二字上做文章。而通过各种限制寻求一个可行解的算法,像极了2-SAT。所以来看一看2-SAT是否可做。

2-SAT的特点是对于每一个变量只有两种取值。我们现在有 n n n个数,每一个数有 k k k个取值,这和2-SAT本身是矛盾的。所以我们要改变一下,将每一个变量定义为每一个位置是否选取 x ( 1 ≤ x ≤ k ) x(1\leq x\leq k) x(1xk)。我们下面用 ( i , x ) (i,x) (i,x)表示 a i = x a_i=x ai=x,用 ! ( i , x ) !(i,x) !(i,x)表示 a i ≠ x a_i\neq x ai=x

在这种定义之下,我们考虑建图。建图需要一些变量之间的关系。在这道题目中,规定我们要构造的序列必须是一个不降的序列,所以对于每一个 ( i , x ) (i,x) (i,x),如果选择了 ( i , x ) (i,x) (i,x),那么对于所有的 c ( i ≤ c ≤ n ) c(i\leq c\leq n) c(icn),就不能选择所有的 ( c , j ) ( 1 ≤ j < x ) (c,j)(1\leq j < x) (c,j)(1j<x);对于所有的 c ( 1 ≤ c < i ) c(1\leq c<i) c(1c<i),就不能选择所有的 ( c , j ) ( x < j ≤ k ) (c,j)(x<j\leq k) (c,j)(x<jk)

然而这样的连边方式会导致图的边数非常庞大,因为对于每一个位置,都要连出 n k nk nk规模的边,那么 n n n个位置就要连 n 2 k n^2k n2k规模的边。这显然是难以实现的。

但是我们可以对于2-SAT换一个定义变量的方式:如果我们定义每一个变量为 ( i , x ) (i,x) (i,x),表示 a i ≥ x a_i\geq x aix,用 ! ( i , x ) !(i,x) !(i,x)表示 a i < x a_i<x ai<x。利用这个定义尝试重新建图。当我们从赋值变成范围之后,整个建图就变得非常简洁了。

这种定义下,我们可以通过 i i i x x x来对 ( i , x ) (i,x) (i,x)进行一个编号。由于每一个位置至多有 k k k个取值,所以我们可以令 ( i , x ) (i,x) (i,x)的编号为 ( i − 1 ) k + x (i-1)k+x (i1)k+x。这样可以选择的情况共有 n k nk nk种。根据2-SAT,我们还要对不选择的情况进行编号。所以 ! ( i , x ) !(i,x) !(i,x)的编号是 ( i − 1 ) k + x + n k (i-1)k+x+nk (i1)k+x+nk。(当然也可以采取别的编号方式)

我们设 i d ( i , x ) id(i,x) id(i,x)表示 ( i , x ) (i,x) (i,x)的编号,那么 i d ( i , x ) id(i,x) id(i,x)的求法是:

int id(int i, int x) {
    
    
	if (i < 1 or i > n or x < 1 or x > k) return -1e9;
	else return (i - 1) * k + x;
}

因为可能会在后面询问一些不存在的点,为了保证到时候给建边的函数一个“这个是不合法情况”的信号,所以在不存在的点进行查询的时候,返回一个很小的值,这样建边的时候,只要传来的编号是负数,就直接返回,不建边。

建边函数addEdge如下:

struct Edge {
    
    
	int v, next;
}e[N * 22];

void addEdge(int u, int v) {
    
    
	if (u <= 0 or v <= 0) {
    
    
		return;
	}
	e[++en] = {
    
    v, front[u]};
	front[u] = en;
}

从构造不降序列的角度来看:如果 a i ≥ x a_i\geq x aix,那么必然有 a i + 1 ≥ x a_{i+1}\geq x ai+1x;必然有 a i ≥ x − 1 a_i\geq x-1 aix1。同样,如果 a i + 1 < x a_{i+1}<x ai+1<x,那么必然有 a i < x a_i<x ai<x。如果 a x < x − 1 a_x<x-1 ax<x1,则必然有 a x < x a_x<x ax<x

根据这些关系,先进行一个初始的限制的建图,代码如下。

for (int i = 1; i <= n; ++i) {
    
    
		for (int j = 1; j <= k; ++j) {
    
    
			addEdge(id(i, j), id(i + 1, j));
			addEdge(id(i, j), id(i, j - 1));
			addEdge(id(i, j - 1) + n * k, id(i, j) + n * k);
			addEdge(id(i + 1, j) + n * k, id(i, j) + n * k);
		}
	}

有一点特殊的是,如果 a 1 < 1 a_1<1 a1<1,那么 a 1 ≥ 1 a_1\geq 1 a11必须要成立。这个是必须满足的条件,所以一旦有条件指向了 a 1 < 1 a_1<1 a1<1,这样建边后,会令其恢复成 a 1 ≥ 1 a_1\geq 1 a11的状态,避免出错。

接下来考虑给定的 3 3 3种限制条件该如何转化成我们建边的形式。
(为了写代码的时候避开循环变量 i , j i,j i,j,所以这里取的字母跟题面不同)

对于第一种限制条件: a x ≠ d a_x\neq d ax=d,我们可以把这个语义转换为如果 a x ≥ d a_x\geq d axd,那么 a x ≥ d + 1 a_x\geq d+1 axd+1必然成立;如果 a x < d + 1 a_x<d+1 ax<d+1成立,那么 a x < d a_x<d ax<d也必然成立。把这个语义转化为我们的状态,那就是: ( x , d ) → ( x , d + 1 ) (x,d)\rightarrow (x,d+1) (x,d)(x,d+1) ! ( x , d + 1 ) → ! ( x , d ) !(x,d+1)\rightarrow !(x,d) !(x,d+1)!(x,d)
这里要注意,如果 d = k d=k d=k,那么转换的语义就将是 a x < k a_x<k ax<k

对于第二种限制条件: a x + a y ≤ d a_x+a_y\leq d ax+ayd。我们设 a x ≥ j a_x\geq j axj,那么有 j ≤ a i ≤ d − a y j\leq a_i\leq d-a_y jaiday,从而推出 a y ≤ d − j a_y\leq d-j aydj,即 a y < d − j + 1 a_y<d-j+1 ay<dj+1。同理互换 x , y x,y x,y可以得到如果 a y ≥ j a_y\geq j ayj,那么 a x < d − j + 1 a_x<d-j+1 ax<dj+1
反过来说,如果 a y ≥ d − j + 1 a_y\geq d-j+1 aydj+1,那么 a x < d a_x<d ax<d也必然成立。互换 x , y x,y x,y同理。
将语义转化为状态,可以得到: ( x , j ) → ! ( y , d − j + 1 ) (x,j)\rightarrow !(y,d-j+1) (x,j)!(y,dj+1) ( y , d − j + 1 ) → ! ( x , j ) (y,d-j+1)\rightarrow !(x,j) (y,dj+1)!(x,j) ( y , j ) → ! ( x , d − j + 1 ) (y,j)\rightarrow !(x,d-j+1) (y,j)!(x,dj+1) ( x , d − j + 1 ) → ! ( y , j ) (x,d-j+1)\rightarrow !(y,j) (x,dj+1)!(y,j)
这里要注意的是,如果 d ≤ k d\leq k dk,那么可能会存在 a x = d , a y = 0 a_x=d,a_y=0 ax=d,ay=0 a x = 0 , a y = d a_x=0,a_y=d ax=0,ay=d的情况。为了避免这种情况发生,所以我们要求,如果 a x ≥ d a_x\geq d axd,那么 a x < d a_x<d ax<d;如果 a y ≥ d a_y\geq d ayd,那么 a y < d a_y<d ay<d
转换语义后得到: ( x , d ) → ! ( x , d ) (x,d)\rightarrow !(x,d) (x,d)!(x,d) ( y , d ) → ! ( y , d ) (y,d)\rightarrow !(y,d) (y,d)!(y,d)

对于第三种限制条件: a x + a y ≥ d a_x+a_y\geq d ax+ayd。我们社 a x < j a_x<j ax<j,那么有 j > a x ≥ d − a y j>a_x\geq d-a_y j>axday,从而推出 a y > d − j a_y>d-j ay>dj,即 a y ≥ d − j + 1 a_y\geq d-j+1 aydj+1。同理互换 x , y x,y x,y可以得到如果 a y < j a_y<j ay<j,那么 a x ≥ d − j + 1 a_x\geq d-j+1 axdj+1
反过来说,如果 a y < d − j + 1 a_y<d-j+1 ay<dj+1,那么 a x ≥ j a_x\geq j axj也必然成立。互换 x , y x,y x,y同理。
将语义转化为状态,可以得到 ! ( x , j ) → ( y , d − j + 1 ) !(x,j)\rightarrow (y,d-j+1) !(x,j)(y,dj+1) ( x , j ) → ! ( y , d − j + 1 ) (x,j)\rightarrow !(y,d-j+1) (x,j)!(y,dj+1) ! ( y , j ) → ( x , d − j + 1 ) !(y,j)\rightarrow (x,d-j+1) !(y,j)(x,dj+1) ( y , j ) → ! ( x , d − j + 1 ) (y,j)\rightarrow !(x,d-j+1) (y,j)!(x,dj+1)
这里要注意的是:如果 d > k d>k d>k,为了避免 a x , a y a_x,a_y ax,ay中有一个数大于 k k k,即另外一个数小于 d − k d-k dk,所以我们要求,如果 a x < d − k a_x<d-k ax<dk,那么 a x ≥ d − k a_x\geq d-k axdk;如果 a y < d − k a_y<d-k ay<dk,那么 a y ≥ d − k a_y\geq d-k aydk
转换语义后得到: ! ( x , d − k ) → ( x , d − k ) !(x,d-k)\rightarrow (x,d-k) !(x,dk)(x,dk) ! ( y , d − k ) → ( y , d − k ) !(y,d-k)\rightarrow (y,d-k) !(y,dk)(y,dk)

建边结束后,走一遍tarjan算法锁点,求强连通分量。

对于这 n k nk nk个情况,我们分别看看每一个情况取或者不取是否在同一个强联通分量中,如果在,则直接输出 − 1 -1 1

如果都不在同一个强联通分量中,那么我们的答案一定存在。接下来构造序列。

对于 n n n个位置的每一个位置,我们都尝试从大数向小数选取,如果有一个可行取值,那么就直接填入答案并退出。根据我们对状态的定义,是 a i ≥ x a_i\geq x aix,所以我们从大到小遍历所有可能的取值,如果 a i ≥ d a_i\geq d aid满足而 a i ≥ d + 1 a_i\geq d+1 aid+1不满足,那么 a i = d a_i=d ai=d a i a_i ai可选的最大值。选择最大值的好处是不需要再看其他的取值的同时,不会因为这个位置选择的值小而导致序列不再满足不减性质。

具体判断这个数是否可行,只需要判断第 i i i个位置取 j j j这个数的情况中,选取 ( i , j ) (i,j) (i,j)的状态所在的强联通分量编号和 ! ( i , j ) !(i,j) !(i,j)的状态所在的强联通分量的大小关系,如果 ( i , j ) (i,j) (i,j)更小,那么就选择 ( i , j ) (i,j) (i,j)(可以认为 ( i , j ) (i,j) (i,j)的值为真,就等同于洛谷的那道模板题的输出解的做法)。

最后输出数组即可。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e5 + 5;

int n, m, k, en;
int front[N * 22], stk[N * 22], ins[N * 22], low[N * 22], dfn[N * 22];
int color[N * 22], ans[N * 2];
int di, si, colcnt;

struct Edge {
    
    
	int v, next;
}e[N * 22];

void addEdge(int u, int v) {
    
    
	if (u <= 0 or v <= 0) {
    
    
		return;
	}
	e[++en] = {
    
    v, front[u]};
	front[u] = en;
}

void tarjan (int u) {
    
    
	dfn[u] = low[u] = ++di;
	stk[++si] = u; ins[u] = 1;
	for (int i = front[u]; i; i = e[i].next) {
    
    
		int v = e[i].v;
		if (dfn[v] == 0) {
    
    
			tarjan(v); low[u] = min(low[u], low[v]);
		}
		else if (ins[v]) low[u] = min(low[u], dfn[v]);
	}
	if (dfn[u] == low[u]) {
    
    
		colcnt++;
		while (true) {
    
    
			int v = stk[si--]; ins[v] = 0;
			color[v] = colcnt;
			if (u == v) break;
		}
	}
}

int id(int i, int x) {
    
    
	if (i < 1 or i > n or x < 1 or x > k) return -1e9;
	else return (i - 1) * k + x;
}

void main2() {
    
    
	cin >> n >> m >> k;
	en = di = si = colcnt = 0;
	for (int i = 1; i <= n * k * 2; ++i) {
    
    
		low[i] = dfn[i] = front[i] = color[i] = ins[i] = 0;
	}
	for (int i = 1; i <= n; ++i) {
    
    
		for (int j = 1; j <= k; ++j) {
    
    
			addEdge(id(i, j), id(i + 1, j));
			addEdge(id(i, j), id(i, j - 1));
			addEdge(id(i, j - 1) + n * k, id(i, j) + n * k);
			addEdge(id(i + 1, j) + n * k, id(i, j) + n * k);
		}
	}
	addEdge(id(1, 1) + n * k, id(1, 1));
	for (int i = 1; i <= m; ++i) {
    
    
		int o, x, y, d;
		cin >> o;
		if (o == 1) {
    
    
			cin >> x >> d;
			if (d == k) addEdge(id(x, d), id(x, d) + n * k);
			else {
    
    
				addEdge(id(x, d), id(x, d + 1));
				addEdge(id(x, d + 1) + n * k, id(x, d) + n * k);
			}
		}
		else if (o == 2) {
    
    
			cin >> x >> y >> d;
			if (d <= k) {
    
    
				addEdge(id(x, d), id(x, d) + n * k);
				addEdge(id(y, d), id(y, d) + n * k);
			}
			for (int j = 1; j <= k; ++j) {
    
    
				addEdge(id(x, j), id(y, d - j + 1) + n * k);
				addEdge(id(y, d - j + 1), id(x, j) + n * k);
				addEdge(id(y, j), id(x, d - j + 1) + n * k);
				addEdge(id(x, d - j + 1), id(y, j) + n * k);
			}
		}
		else {
    
    
			cin >> x >> y >> d;
			if (d > k) {
    
    
				addEdge(id(x, d - k) + n * k, id(x, d - k));
				addEdge(id(y, d - k) + n * k, id(y, d - k));
			}
			for (int j = 1; j <= k; ++j) {
    
    
				addEdge(id(x, j) + n * k, id(y, d - j + 1));
				addEdge(id(y, d - j + 1) + n * k, id(x, j));
				addEdge(id(y, j) + n * k, id(x, d - j + 1));
				addEdge(id(x, d - j + 1) + n * k, id(y, j));
			}
		}
	}
	for (int i = 1; i <= n * k * 2; ++i) {
    
    
		if (!dfn[i]) tarjan(i);
	}
	for (int i = 1; i <= n; ++i) {
    
    
		for (int j = 1; j <= k; ++j) {
    
    
			if (color[id(i, j)] == color[id(i, j) + n * k]) {
    
    
				cout << -1 << '\n';
				return;
			}
		}
		for (int j = k; j >= 1; --j) {
    
    
			if (color[id(i, j)] < color[id(i, j) + n * k]) {
    
    
				ans[i] = j; 
				break;
			}
		}
	}
	for (int i = 1; i <= n; ++i) {
    
    
		cout << ans[i] << ' ';
	}
	cout << '\n';
}

int main() {
    
    
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	LL _ = 1;
	cin >> _;
	while (_--) main2();
	return 0;
}

猜你喜欢

转载自blog.csdn.net/xhyu61/article/details/126002961
今日推荐