SNOI 2019 题解

[SNOI2019]纸牌
又是麻将 d p dp dp,令 f i , j , k f_{i,j,k} fi,j,k 表示 ( i , i + 1 , i + 2 ) (i,i+1,i+2) (i,i+1,i+2) 选了 j j j 个, ( i − 1 , i , i + 1 ) (i-1,i,i+1) (i1,i,i+1) 选了 k k k 的方案数
f i , l , j < − f i − 1 , j , k ∗ ( C − l − j − k 3 + 1 ) f_{i,l,j}<-f_{i-1,j,k}*(\frac{C-l-j-k}{3}+1) fi,l,j<fi1,j,k(3Cljk+1)
( j , k ) (j,k) (j,k) 表示成一维,中间已经有的点暴力转移,其余矩阵乘法即可

#include<bits/stdc++.h>
#define N 9
#define M 1050
using namespace std;
typedef long long ll;
const int Mod = 998244353;
int add(int a, int b){ return a + b >= Mod ? a + b - Mod : a + b;}
int mul(int a, int b){ return 1ll * a * b % Mod;}
void Add(int &a, int b){ a = add(a, b);}
int C, X, a[M];
ll n, p[M]; 
struct Matrix{
	int a[N][N];
	Matrix(){ memset(a, 0, sizeof(a));}
	Matrix operator * (const Matrix &A){
		Matrix B; for(int i = 0; i < 9; i++)
			for(int j = 0; j < 9; j++) for(int k = 0; k < 9; k++)
				Add(B.a[i][j], mul(a[i][k], A.a[k][j]));
		return B;
	}
};
Matrix power(Matrix A, ll b){
	Matrix ans; for(int i = 0; i < 9; i++) ans.a[i][i] = 1;
	for(;b;b>>=1){if(b&1) ans = ans*A; A = A*A;} return ans;
}
int main(){
	scanf("%lld%d%d", &n, &C, &X);
	for(int i = 1; i <= X; i++){
		scanf("%lld%d", &p[i], &a[i]);
	} Matrix A, B; A.a[0][0] = 1;
	for(int i = 0; i < 3; i++)
		for(int j = 0; j < 3; j++)
			for(int k = 0; k < 3; k++)
				if(i + j + k <= C) B.a[j*3+k][i*3+j] = (C-i-j-k) / 3 + 1;
	for(int d = 1; d <= X; d++){
		A = A * power(B, p[d] - 1 - p[d-1]);
		Matrix tmp;
		for(int i = 0; i < 3; i++){
			for(int j = 0; j < 3; j++){
				for(int k = 0; k < 3; k++){
					int x = i * 3 + j, y = j * 3 + k;
					int now = i + j + k, must = (now >= a[d]) ? now : (a[d] + ((now - a[d]) % 3 + 3) % 3);
					if(must <= C) tmp.a[y][x] = (C - must) / 3 + 1;
				}
			}
		} A = A * tmp;
	}
	A = A * power(B, n - p[X]);
	cout << A.a[0][0]; return 0;
}

[SNOI2019]通信
网络流模型显然, i i i j ′ j' j ∣ a i − a j ∣ |a_i-a_j| aiaj 的边再向 T T T W W W 的边即可
边数达到了 O ( n 2 ) O(n^2) O(n2) 凉凉
发现 i i i 会向它前面的比它小的连 a i − a j a_i-a_j aiaj 的边,比它大的连 a j − a i a_j-a_i ajai 的边
建两棵主席树,一棵底层向原节点连正边,一棵连负边
还有一种分治优化建图的巧妙思想
考虑 c d q cdq cdq,每次解决后一半的向前一半连边的问题
把权值嗲出来排成一排排序去重,相邻连边 a i + 1 − a i a_{i+1}-a_i ai+1ai
这样的好处是从一个点进一个点出中间的可以抵消掉
然后对一个点找到权值,如果在前一半就连出边,后一半就连入即可

#include<bits/stdc++.h>
#define N 200050
#define M 3000050
using namespace std;
int read(){
	int cnt = 0, f = 1; char ch = 0;
	while(!isdigit(ch)){ ch = getchar(); if(ch=='-') f = -1; } 
	while(isdigit(ch)) cnt = cnt*10 + (ch-'0'), ch = getchar();
	return cnt * f;
}
int first[N], nxt[M], to[M], w[M], c[M], tot = 1;
void add(int x, int y, int z, int v){
	nxt[++tot] = first[x], first[x] = tot, to[tot] = y, w[tot] = z, c[tot] = v;
	nxt[++tot] = first[y], first[y] = tot, to[tot] = x, w[tot] = 0, c[tot] = -v;
}
typedef long long ll;
int n, W;
int a[N], b[N], len;
int rt1, rt2, node, ls[N], rs[N];
int st, ed;
#define mid ((l+r)>>1)
void Add(int x, int l, int r, int L, int R, int pos, int v){
	if(!x) return;
	if(L<=l && r<=R){ add(pos, x, 1, v); return;}
	if(L <= mid) Add(ls[x], l, mid, L, R, pos, v);
	if(R > mid) Add(rs[x], mid+1, r, L, R, pos, v);
}
void ins(int las, int &x, int l, int r, int p, int pos, int v){
	x = ++node; ls[x] = ls[las]; rs[x] = rs[las];
	if(l == r){ add(x, pos, 1, v); return;} 
	if(p <= mid) ins(ls[las], ls[x], l, mid, p, pos, v);
	else ins(rs[las], rs[x], mid+1, r, p, pos, v);
	if(ls[x]) add(x, ls[x], 1e9, 0);
	if(rs[x]) add(x, rs[x], 1e9, 0);
}
ll dis[N]; bool vis[N]; int from[N], froms[N];
bool spfa(){
	for(int i = 0; i <= node; i++) dis[i] = 1e18, vis[i] = 0;
	dis[st] = 0; queue<int> q; q.push(st);
	while(!q.empty()){
		int x = q.front(); q.pop(); vis[x] = 0;
		for(int i = first[x]; i; i = nxt[i]){
			int t = to[i]; if(w[i] && dis[t] > dis[x] + c[i]){
				from[t] = x; froms[t] = i;
				dis[t] = dis[x] + c[i]; if(!vis[t]) q.push(t), vis[t] = 1;
			}
		}
	} return dis[ed] != 1e18;
}
int calc(){
	int flow = 1e9, u = ed;
	while(u^st) flow = min(flow, w[froms[u]]), u = from[u]; u = ed;
	while(u^st) w[froms[u]] -= flow, w[froms[u]^1] += flow, u = from[u];
	return flow;	
}
ll dinic(){ ll ans = 0; while(spfa()) ans += 1ll * calc() * dis[ed]; return ans;}
int main(){
	n = read(), W = read();
	for(int i = 1; i <= n; i++) a[i] = b[++len] = read();
	sort(b + 1, b + len + 1); len = unique(b + 1, b + len + 1) - (b + 1);
	for(int i = 1; i <= n; i++) a[i] = lower_bound(b + 1, b + len + 1, a[i]) - b;
	st = 0; ed = node = 2 * n + 1;
	for(int i = 1; i <= n; i++){
		add(st, i, 1, 0); add(i + n, ed, 1, 0);
		add(i, ed, 1, W);
	}
	for(int i = 1; i <= n; i++){
		Add(rt1, 1, len, 1, a[i], i, b[a[i]]);
		Add(rt2, 1, len, a[i], len, i, -b[a[i]]);
		ins(rt1, rt1, 1, len, a[i], n + i, -b[a[i]]);
		ins(rt2, rt2, 1, len, a[i], n + i, b[a[i]]);
	} cout << dinic(); return 0;
}

[SNOI2019]数论
对于 A A A 集合中的一个数 a i a_i ai, 显然 a i + k ∗ p a_i+k*p ai+kp 都是合法的
k ≤ T − 1 − a i p k\le \frac{T-1-a_i}{p} kpT1ai,而如果这样一直加下去,在模 q q q 意义下一定会循环
考虑循环节大小: l c m ( p , q ) p = q g c d ( p , q ) \frac{lcm(p,q)}{p}=\frac{q}{gcd(p,q)} plcm(p,q)=gcd(p,q)q,发现不是很大
于是有一个巧妙的转换,类似同余最短路之类的东西
对于 x ∈ [ 0 , q ) x\in[0,q) x[0,q) x x x ( x + p ) % q (x+p)\%q (x+p)%q 连边,如果属于 B B B 集合那么权值为 1 1 1
问题转换为从一个 x x x 开始,走 T − 1 − x p \frac{T-1-x}{p} pT1x 步,经过的权值是多少
考虑到最后的图一定是一个个大小为 q g c d ( p , q ) \frac{q}{gcd(p,q)} gcd(p,q)q 的环
对环统计一个前缀和就可以 O ( 1 ) O(1) O(1) 知道答案
这种数学问题通过图论建模的转换十分巧妙

#include<bits/stdc++.h>
#define N 1000050
using namespace std;
typedef long long ll;
int n, m, P, Q, tot;
vector<int> cir[N], sum[N];
int A[N], B[N], vis[N], v[N], pos[N], val[N];
ll T, p[N];
int dfs(int u){
	if(vis[u]) return 0;
	vis[u] = tot; cir[tot].push_back(u); pos[u] = cir[tot].size() - 1;
	return dfs((u + P) % Q) + v[u];
}
int gcd(int a, int b){ return !b ? a : gcd(b, a % b);}
int main(){
	scanf("%d%d%d%d%lld", &P, &Q, &n, &m, &T);
	for(int i = 1; i <= n; i++) scanf("%d", &A[i]);
	for(int i = 1; i <= m; i++) scanf("%d", &B[i]);
	if(P > Q) swap(P, Q), swap(n, m), swap(A, B);
	for(int i = 1; i <= m; i++) v[B[i]] = 1;
	for(int i = 1; i <= n; i++) p[i] = (T - 1 - A[i]) / P;
	for(int i = 0; i < Q; i++){
		if(!vis[i]) val[++tot] = dfs(i);
	}
	for(int i = 1; i <= tot; i++){
		int Siz = cir[i].size();
		for(int j = 0; j < Siz; j++) cir[i].push_back(cir[i][j]);
		sum[i].push_back(v[cir[i][0]]);
		for(int j = 1; j < cir[i].size(); j++) sum[i].push_back(v[cir[i][j]] + sum[i][j-1]); 
	}
	int len = Q / gcd(P, Q);
	ll ans = 0;
	for(int i = 1; i <= n; i++){
		ans += 1ll * (p[i] / len) * val[vis[A[i]]];
		int res = p[i] % len;
		ans += sum[vis[A[i]]][pos[A[i]] + res] - sum[vis[A[i]]][pos[A[i]]] + v[A[i]];
	} cout << ans; return 0;
}

[SNOI2019]字符串
先把相邻的相同的压成一个
分类讨论:
a i > a i + 1 a_i>a_{i+1} ai>ai+1 那么删除 a i a_i ai 比删除后面的任意一个都更优
a i < a i + 1 a_i<a_{i+1} ai<ai+1 那么删除 a i a_i ai 比删除后面任意一个都不优
然后可以从小到大扫一遍,按上述规则将 a i a_i ai 放到当前的最前面或者最后面即可


[SNOI2019]积木
首先并未要求操作数最小
发现找到空格到终点空格的路径后,中间的经过的块可以顺便归位
但这样就导致了没有经过的地方没有归位
我们就可以暴力把空格挪过去归位后在挪回来
空格在从起点移到终点的过程中,我们看一下哪个方向的格子没有走过
然后把空格挪过去转换为更原来一样的问题

这种构造题做的还是不多
本质思想是通过操作次数来让写法变得更简便

#include<bits/stdc++.h>
#define N 2050
#define cs const
using namespace std;
int n, m, Sx, Sy, Tx, Ty;
char S[N][N], T[N][N];
int dx[5] = {1, 0, -1, 0};
int dy[5] = {0, 1, 0, -1};
int way(char c){
	if(c == 'n') return 0;
	if(c == '<') return 1;
	if(c == 'u') return 2;
	if(c == '>') return 3;
}
cs char *str = "n<u>", *op = "DRUL";

bool used[N][N], vis[N][N];
void go(int d){
	putchar(op[d]);
	int ox = Sx + dx[d], oy = Sy + dy[d], di = way(S[ox][oy]);
	int nx = ox + dx[di], ny = oy + dy[di];
	S[nx][ny] = 'o'; S[Sx][Sy] = str[d]; S[ox][oy] = str[d ^ 2];
	Sx = nx, Sy = ny;
}
void dfs2(int x, int y){
	if(vis[x][y] || (x == Tx && y == Ty)) return;
	int d = way(T[x][y]);
	vis[x][y] = true; x += dx[d], y += dy[d];
	vis[x][y] = true; d = way(S[x][y]); 
	x += dx[d], y += dy[d]; dfs2(x, y);
}
void dfs(int x, int y){
	if(used[x][y]) return; used[x][y] = true;
	dfs2(x, y);
	for(int i = 0; i < 4; i++){
		int nx = x + dx[i], ny = y + dy[i];
		if(nx < 1 || nx > n || ny < 1 || ny > m || vis[nx][ny]) continue;
		go(i); dfs(Sx, Sy);
	}
	if(x == Tx && y == Ty) return;
	go(way(T[x][y])); dfs(Sx, Sy);
}
int main(){
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= n; i++) scanf("%s", S[i] + 1);
	for(int i = 1; i <= n; i++) scanf("%s", T[i] + 1);
	for(int i = 1; i <= n; i++)
		for(int j = 1; j <= m; j++)
			if(S[i][j] == 'o') Sx = i, Sy = j;
	for(int i = 1; i <= n; i++)
		for(int j = 1; j <= m; j++)
			if(T[i][j] == 'o') Tx = i, Ty = j;
	dfs(Sx, Sy); return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FSYo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值