2024.7 - 做题记录与方法总结-Part2

2024/07/09

校内模拟赛 #1

Codeforces Round 936 (Div. 2) -> E. Girl Permutation

题面:

某个长度为 n n n 的排列需要被猜中。

您将得到其前缀最大值和后缀最大值的索引。

长度为 k k k 的排列是一个大小为 k k k 的数组,从 1 1 1 k k k 的每个整数都正好出现一次。

前缀最大值是指在以该元素结尾的前缀上最大的元素。更正式地说,如果每个 j j j < i i i a i a_i ai > a j a_j aj 都是前缀最大值,那么元素 a i a_i ai 就是前缀最大值。

同样,后缀最大值的定义是:如果每个 j j j > i i i a i a_i ai > a j a_j aj 都是后缀最大值,那么元素 a i a_i ai 就是后缀最大值。

您需要输出可能被猜中的不同排列组合的数量。

由于这个数字可能非常大,请输出以 1 0 9 + 7 10^9 + 7 109+7 为模数的答案。

首先,观察题目,不难看出指定位置(前、后缀最大值位置)形成一个 ,如图:

img

然后,我们不急着去想排列组合

先否定掉最错误的答案———— p 1 p_1 p1 显然是 1 1 1 p m 1 p_{m_1} pm1 显然和 s 1 s_1 s1 相等形成 s m 2 s_{m_2} sm2 显然是 n n n

那么对于不满足上述要求的一律去掉

接下来,我们就可以根据最大值两侧分开分析

我们先看最大值左侧

下文中, a n s ans ans 初值为 1 1 1

当我们确定最大值时,左侧 n − 1 n - 1 n1 个数可以被选,有 p m 1 − 1 p_{m_1} - 1 pm11 个位置,那么 a n s ← a n s ⋅ ( n − 1 p m 1 − 1 ) ans \leftarrow ans \cdot \dbinom{n - 1}{p_{m_1} - 1} ansans(pm11n1)

再将 p m 1 − 1 p_{m_1 - 1} pm11 位置为界,将左侧又分成 1 ∼ ( p m 1 − 1 − 1 ) 1 \sim (p_{m_1-1} - 1) 1(pm111) ( p m 1 − 1 + 1 ) ∼ ( p m 1 − 1 ) (p_{m_1 - 1} + 1) \sim (p_{m_1} - 1) (pm11+1)(pm11) 两部分

所以 a n s ← a n s ⋅ ( p m 1 − 2 p m 1 − 1 ) ans \leftarrow ans \cdot \dbinom{p_{m_1} - 2}{p_{m_1} - 1} ansans(pm11pm12)

而且 ( p m 1 − 1 + 1 ) ∼ ( p m 1 − 1 ) (p_{m_1 - 1} + 1) \sim (p_{m_1} - 1) (pm11+1)(pm11) 这部分的值可以进行排列

又有 a n s ← a n s ⋅ ( p m 1 − p m 1 − 1 + 1 ) ! ans \leftarrow ans \cdot (p_{m_1} - p_{m_1 - 1} + 1)! ansans(pm1pm11+1)!

以此类推,直到分到 p 1 p_1 p1 结束

对于 s s s,自然同理即可

AC-code:

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

#define int long long

int rd() {
	int x = 0, w = 1;
	char ch = 0;
	while (ch < '0' || ch > '9') {
		if (ch == '-') w = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + (ch - '0');
		ch = getchar();
	}
	return x * w;
}

void wt(int x) {
	static int sta[35];
	int f = 1;
	if(x < 0) f = -1,x *= f;
	int top = 0;
	do {
		sta[top++] = x % 10, x /= 10;
	} while (x);
	if(f == -1) putchar('-');
	while (top) putchar(sta[--top] + 48);
}

const int mod = 1e9+7;

const int N = 2e5+5;

int fac[N],inv[N];

int qpow(int x,int k) {
	int res = 1;
	while(k) {
		if(k & 1) res = (res * x) % mod;
		x = (x * x) % mod;
		k >>= 1;
	}
	return res;
}

int dbinom(int x,int y) {
	if(x < y) return 0;
	return fac[x] * inv[y] % mod * inv[x - y] % mod;
}

void solve() {
	int n = rd(),m1 = rd(),m2 = rd();

	vector<int> p(m1 + 1),s(m2 + 1);
	for(int i = 1;i<=m1;i++) p[i] = rd();
	for(int i = 1;i<=m2;i++) s[i] = rd();
	if(p[1] != 1 || p[m1] != s[1] || s[m2] != n) {puts("0");return;}

	int res = dbinom(n - 1,p[m1] - 1);
	for(int i = m1 - 1;i >= 1;i--) {
		res = (res * dbinom(p[i + 1] - 2,p[i] - 1)) % mod;
		res = (res * fac[p[i + 1] - p[i] - 1]) % mod;
	}
	for(int i = 2;i<=m2;i++) {
		res = (res * dbinom(n - s[i - 1] - 1,n - s[i])) % mod;
		res = (res * fac[(s[i] - s[i-1] - 1)]) % mod;
	}
	wt(res),putchar('\n');
}

signed main() {
	fac[0] = fac[1] = 1;
	for(int i = 2;i<=2e5;i++) fac[i] = (fac[i-1] * i) % mod;
	inv[0] = inv[1] = 1;
	for(int i = 2;i<=2e5;i++) inv[i] = (mod - mod / i) * inv[mod%i] % mod;
	for(int i = 2;i<=2e5;i++) inv[i] = inv[i-1] * inv[i] % mod;

	int t = rd();
	while(t--) solve();	

	return 0;
}
P3924 康娜的线段树

题面:

题目描述

小林是个程序媛,不可避免地康娜对这种人类的“魔法”产生了浓厚的兴趣,于是小林开始教她OI。

今天康娜学习了一种叫做线段树的神奇魔法,这种魔法可以维护一段区间的信息,是非常厉害的东西。康娜试着写了一棵维护区间和的线段树。由于她不会打标记,因此所有的区间加操作她都是暴力修改的。具体的代码如下:

struct Segment_Tree{
#define lson (o<<1)
#define rson (o<<1|1)
   int sumv[N<<2],minv[N<<2];
   inline void pushup(int o){sumv[o]=sumv[lson]+sumv[rson];}
   inline void build(int o,int l,int r){
       if(l==r){sumv[o]=a[l];return;}
       int mid=(l+r)>>1;
       build(lson,l,mid);build(rson,mid+1,r);
       pushup(o);
   }
   inline void change(int o,int l,int r,int q,int v){
       if(l==r){sumv[o]+=v;return;}
       int mid=(l+r)>>1;
       if(q<=mid)change(lson,l,mid,q,v);
       else change(rson,mid+1,r,q,v);
       pushup(o);
   }
}T; 

在修改时,她会这么写:

for(int i=l;i<=r;i++)T.change(1,1,n,i,addv);

显然,这棵线段树每个节点有一个值,为该节点管辖区间的区间和。

康娜是个爱思考的孩子,于是她突然想到了一个问题:

如果每次在线段树区间加操作做完后,从根节点开始等概率的选择一个子节点进入,直到进入叶子结点为止,将一路经过的节点权值累加,最后能得到的期望值是多少?

康娜每次会给你一个值 q w q qwq qwq ,保证你求出的概率乘上 q w q qwq qwq 是一个整数。

这个问题太简单了,以至于聪明的康娜一下子就秒了。

现在她想问问你,您会不会做这个题呢?

输入格式

第一行整数 n , m , q w q n,m,qwq n,m,qwq 表示线段树维护的原序列的长度,询问次数,分母。

第二行 n n n 个数,表示原序列。

接下来 m m m 行,每行三个数 l , r , x l,r,x l,r,x 表示对区间 [ l , r ] [l,r] [l,r] 加上 x x x

输出格式

m m m 行,表示期望的权值和乘上qwq结果。

样例 #1
样例输入 #1
8 2 1
1 2 3 4 5 6 7 8
1 3 4
1 8 2
样例输出 #1
90
120
提示

对于30%的数据,保证 1 ≤ n , m ≤ 100 1 \leq n,m \leq 100 1n,m100

对于70%的数据,保证 1 ≤ n , m , ≤ 1 0 5 1 \leq n,m, \leq 10^{5} 1n,m,105

对于100%的数据,保证$1 \leq n,m \leq 10^6 $

− 1000 ≤ a i , x ≤ 1000 -1000 \leq a_i,x \leq 1000 1000ai,x1000

这个线段树成功让我在赛时卡了足足 5 h 5h 5h 还只拿了 50 50 50

但是,这道题让我找到了造新题的思路

先上场的是我的难绷思路:(大家可以直接跳转到 正解

我们可以注意到

根据 期望定义,$ 期望 = (基本事件值总和) / 基本事件数 $

基本事件数不变,那么每次询问只改变 基本事件总和

所以初始数组,每一次询问,都是可以分开分析的

面对这样一个线段树,如图:

img

因为线段树只有最后一层不满,因此,我们可以发现
P ( 每个以最后一层叶子节点为终点的路径的概率 ) = 1 2 P ( 每个以倒数第 2 层的叶子节点为终点的路径概率 ) P(每个以最后一层叶子节点为终点的路径的概率) = \frac{1}{2}P(每个以倒数第 2 层的叶子节点为终点的路径概率) P(每个以最后一层叶子节点为终点的路径的概率)=21P(每个以倒数第2层的叶子节点为终点的路径概率)

我们就让 前者 = 1 前者 = 1 前者=1 后者 = 2 后者 = 2 后者=2,建一颗辅助线段树去查询每个节点被访问的次数

img

然后再 b u i l d build build 一颗正常线段树,让 a 数组的贡献 → a n s a 数组的贡献 \rightarrow ans a数组的贡献ans

初始数组搞定,然后是每一次询问


当我们模拟过程时,将每一次累加值都拆开

就会发现:
   令 v = 询问值   ,    l e n = 询问覆盖的区间长度   ,    t = 访问总次数 v = 询问值\ \ ,\ \ len = 询问覆盖的区间长度\ \ ,\ \ t = 访问总次数 v=询问值  ,  len=询问覆盖的区间长度  ,  t=访问总次数

   那么,对于任意一个节点,贡献为 v ⋅ l e n ⋅ t v \cdot len \cdot t vlent

   又因为对于每一次询问, v v v 始终是定值,

   那么,对于一段询问区间,有:

贡献总和 = ∑ i ∈ 涵盖 l , r 的节点 v l e n i t i =   v ∑ i ∈ 被 l , r 覆盖的节点 l e n i t i 贡献总和 = \sum_{i \in 涵盖l,r的节点} vlen_it_i =\ v\sum_{i \in 被l,r覆盖的节点}len_it_i 贡献总和=i涵盖l,r的节点vleniti= vil,r覆盖的节点leniti

那么,用一颗树剖树去维护这个 ‘ ∑ \sum ’,难绷的思路就结束了

DEAD-code:

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

#define int __int128

int rd() {
	int x = 0, w = 1;
	char ch = 0;
	while (ch < '0' || ch > '9') {
		if (ch == '-') w = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + (ch - '0');
		ch = getchar();
	}
	return x * w;
}

void wt(int x) {
	static int sta[35];
	int f = 1;
	if(x < 0) f = -1,x *= f;
	int top = 0;
	do {
		sta[top++] = x % 10, x /= 10;
	} while (x);
	if(f == -1) putchar('-');
	while (top) putchar(sta[--top] + 48);
}

const int N = 1e6+5;
bool Mbe;
int n,m,q,a[N];

int nxt[N<<1],to[N<<1],cnt,head[N];

void init() {
	memset(head,-1,sizeof(head));
}

void add(int u,int v) {
	nxt[cnt] = head[u];
	to[cnt] = v;
	head[u] = cnt++;
}

int qt[N<<2];

namespace Segtree_2{
#define ls (p << 1)
#define rs (ls | 1)
#define mid ((pl + pr) >> 1)

void push_up(int p) {
	qt[p] = qt[ls] + qt[rs];
}

void build(int p,int pl,int pr) {
	if(pl == pr) {qt[p]++;return;}
	build(ls,pl,mid);
	if(mid + 1 == pr && pl != mid) qt[rs]++;
	build(rs,mid+1,pr);
	push_up(p);
}

void build_QTree(int p,int pl,int pr) {
	int f = (p >> 1);
	qt[p] *= (pr - pl + 1);
	add(f,p);
	add(p,f);
	if(pl == pr) return;
	build_QTree(ls,pl,mid);
	build_QTree(rs,mid+1,pr);
}

#undef ls
#undef rs
#undef mid
}

int fa[N<<2],top[N<<2],dep[N<<2],son[N<<2],siz[N<<2],id[N<<2];

void dfs1(int x,int f){
	fa[x] = f;
	siz[x] = 1;
	dep[x] = dep[f] + 1;
	for(int i = head[x];~i;i = nxt[i]) {
		int y = to[i];
		if(y ^ f) {
			dfs1(y,x);
			siz[x] += siz[y];
			if(siz[son[x]] < siz[y]) son[x] = y;
		}
	}
}

int num,w_new[N<<2];

void dfs2(int x,int topx){
	top[x] = topx;
	id[x] = ++num;
	w_new[num] = qt[x];
	if(!son[x]) return;
	dfs2(son[x],topx);
	for(int i = head[x];~i;i = nxt[i]) {
		int y = to[i];
		if(y ^ fa[x] && y ^ son[x]) dfs2(y,y);
	}
}

namespace QTree{
#define ls (p << 1)
#define rs (ls | 1)
#define mid ((pl + pr) >> 1)

int t[N<<4];
void push_up(int p) {
	t[p] = t[ls] + t[rs];
}
void build(int p,int pl,int pr) {
	if(pl == pr) {
		t[p] = w_new[pl];
		return;
	}
	build(ls,pl,mid);
	build(rs,mid+1,pr);
	push_up(p);
}
int query(int p,int pl,int pr,int l,int r) {
	if(l <= pl && pr <= r) return t[p];
	int res = 0;
	if(l <= mid) res += query(ls,pl,mid,l,r);
	if(r > mid) res += query(rs,mid+1,pr,l,r);
	return res;
}

#undef ls
#undef rs
#undef mid
}

int query_tree(int x) {
	return QTree::query(1,1,num,id[x],id[x] + siz[x] - 1);
}

int ans = 0;

namespace Segtree_1{
#define ls (p << 1)
#define rs (ls | 1)
#define mid ((pl + pr) >> 1)

int t[N<<2];

void push_up_build(int p,int pl,int pr) {
	t[p] = t[ls] + t[rs];
	ans += t[p] * qt[p]/ (pr - pl + 1);
}

void build(int p,int pl,int pr) {
	if(pl == pr) {
		t[p] = a[pl];
		ans += t[p] * qt[p];
		return;
	}
	build(ls,pl,mid);
	build(rs,mid+1,pr);
	push_up_build(p,pl,pr);
}

#undef ls
#undef rs
#undef mid
}

#define ls (p << 1)
#define rs (ls | 1)
int query_range(int p,int pl,int pr,int l,int r,int v) {
	if(l <= pl && pr <= r) return query_tree(p) * v;
	int res = qt[p] / (pr - pl + 1) * v * (min(r,pr) - max(l,pl) + 1),mid = (pl + pr) >> 1;
	if(l <= mid) res += query_range(ls,pl,mid,l,r,v);
	if(r > mid) res += query_range(rs,mid+1,pr,l,r,v);
	return res;
}
#undef ls
#undef rs

bool Med;

signed main() {
// 	freopen("P3924_1.in","r",stdin);
// 	freopen("ans.out","w",stdout);

	fprintf(stderr, "%.3lf MB\n", (&Med - &Mbe) / 1048576.0);
	init();
	n = rd(),m = rd(),q = rd();
	for(int i = 1;i<=n;i++) a[i] = rd();

	Segtree_2::build(1,1,n);
	Segtree_2::build_QTree(1,1,n);
	Segtree_1::build(1,1,n);
	dfs1(1,0);dfs2(1,1);
	QTree::build(1,1,num);
	
	while(m--) {
		int l = rd(),r = rd(),x = rd();
		ans += query_range(1,1,n,l,r,x);
		wt(ans * q * n/ qt[1]);
		putchar('\n');
	}

	return 0;
}

记录:

img

正解

今天太累了,待补

在此感谢ckain大佬的讲解

AC-code:

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

#define int long long

const int N = 1e6+5;

int rd() {
	int x = 0, w = 1;
	char ch = 0;
	while (ch < '0' || ch > '9') {
		if (ch == '-') w = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + (ch - '0');
		ch = getchar();
	}
	return x * w;
}

void wt(int x) {
	static int sta[35];
	int f = 1;
	if(x < 0) f = -1,x *= f;
	int top = 0;
	do {
		sta[top++] = x % 10, x /= 10;
	} while (x);
	if(f == -1) putchar('-');
	while (top) putchar(sta[--top] + 48);
}

int a[N],n,m,qwq,s[N];

int t[N<<2],dep[N<<2],maxd;

#define ls (p << 1)
#define rs (p << 1 | 1)
#define mid ((pl + pr ) >> 1)

void build(int p,int pl,int pr,int d) {
	if(pl == pr) {
		dep[pl] = d;
		maxd = max(d,maxd);
		t[p] = a[pl];
		return;
	}
	build(ls,pl,mid,d + 1);
	build(rs,mid+1,pr,d + 1);
	t[p] = t[ls] + t[rs];
}

int query(int p,int pl,int pr,int T,int tt) {
	if(pl == pr) return (1 << T) * (tt + t[p]);
	return query(ls,pl,mid,T - 1,tt + t[p]) + query(rs,mid+1,pr,T - 1,tt + t[p]);
}

#undef ls 
#undef rs
#undef mid 

signed main() {

	n = rd(),m = rd(),qwq = rd();
	for(int i = 1;i<=n;i++) a[i] = rd();
	build(1,1,n,1);
	int ans = query(1,1,n,maxd - 1,0);
	int y = (1<<(maxd - 1));
	int gcd = __gcd(y,qwq);
	y /= gcd;
	qwq /= gcd;
	for(int i = 1;i<=n;i++) s[i] = s[i-1] + (((1 <<(dep[i])) - 1) << (maxd - dep[i]));
	while(m--) {
		int l = rd(),r = rd(),x = rd();
		ans += (s[r] - s[l - 1]) * x;
		wt(ans / y * qwq),putchar('\n');
	}

	return 0;
}
P8352 [SDOI/SXOI2022] 小 N 的独立集

题面:

[SDOI/SXOI2022] 小 N 的独立集
题目描述

小 N 喜欢出最大权独立集问题。

有一天,他接到了一系列的出题任务,于是他顺手出了一系列最大权独立集问题。

为了同时给这一系列题目造数据,小 N 生成了一个 n n n 个点的树,并选定了一个正整数 k k k。这样每生成一组数据时,他只需要对于每个点,随机生成一个在 1 ∼ k 1 \sim k 1k 之间的整数点权,就可>以生成一个新的最大独立集问题。

小 N 把这些题给了他的好朋友,小 Ω。小 Ω 表示,这些题太多太乱了,他打算把所有的 k n k^n kn 道题归类处理。一个自然的想法就是按答案(也就是最大权独立集中的点的权值之和)分类,显然这>些最大权独立集问题的答案一定在 1 ∼ n k 1 \sim nk 1nk 之间,所以小 Ω 只需要将所有题目按照答案分成 n k nk nk 类进行管理就行了。

在小 N 正式开始出题之前,小 Ω 先要算出每一类题目具体有多少道。稍加估计之后小 Ω 很快意识到自己并没有《诗云》中描述的那种存储器,于是断然拒绝了小 N 关于“先把所有可能的题目造好>再慢慢分类统计数量”的建议,然后悲剧地意识到自己并不会计算这些数字。

他想叫你帮他解决这个问题,还说如果你成功解决了这个问题,那么在小 N 出那些最大权独立集问题的时候,他会帮你提交一份标程代码。

输入格式

第一行, 2 2 2 个正整数 n n n, k k k

接下来 n − 1 n-1 n1 行,每行 2 2 2 个正整数 u i u_i ui, v i v_i vi,描述一条连接点 u i u_i ui v i v_i vi 的边,保证这些边构成一棵树。

输出格式

n k nk nk 行,每行一个整数,第 i i i 个整数表示在所有可能的题目中,最大权独立集大小为 i i i 的有多少道,答案对 1 0 9 + 7 10^9+7 109+7 取模。

样例 #1
样例输入 #1
4 2
1 2
2 3
2 4
样例输出 #1
0
0
2
6
6
2
0
0
提示

【样例解释】

符合题意的最大权独立集题目一共有 2 4 = 16 2^4=16 24=16 道。

可以证明,当点 1 1 1, 3 3 3, 4 4 4 的权值均为 1 1 1 时,最大权独立集为 3 3 3 ,这样的题目共有 2 2 2 道;点 1 1 1, 3 3 3, 4 4 4 的权值恰有一个为 2 2 2 时,最大权独立集为 4 4 4 ,这样的题目共>有 6 6 6 道;对于最大权独立集为 5 5 5 6 6 6 的情况也是类似的。

【数据范围】

对于 15 % 15 \% 15% 的数据, n ≤ 8 n \leq 8 n8
对于 30 % 30 \% 30% 的数据, n ≤ 30 n \leq 30 n30
对于 50 % 50 \% 50% 的数据, n ≤ 100 n \leq 100 n100
对于另外 10 % 10 \% 10% 的数据, k = 1 k=1 k=1
对于另外 15 % 15 \% 15% 的数据, k = 2 k=2 k=2
对于 100 % 100 \% 100% 的数据, n ≤ 1000 n \leq 1000 n1000 k ≤ 5 k \leq 5 k5 u i , v i ≤ n u_{i}, v_{i} \leq n ui,vin

【提示】

最大权独立集问题是指:选择一个点集,使得任意两个被选择的点都没有边直接相连,并且使得所有被选择的点的点权之和最大。

此题思路非常有趣,我觉得这个思路可以叫做 放缩状态优化

首先,我们正常去思考出一个多项式复杂度算法

这不难看出是一道 d p dp dp d p dp dp

那么我们二话不说,先设内层 d p dp dp g i , 0 g_{i,0} gi,0 i i i 节点不取的最大权独立集, g i , 1 g_{i,1} gi,1 i i i 节点取的最大权独立集

所以,自然而然的,我们再设出 f x , i , j f_{x,i,j} fx,i,j 表示在 x x x 节点符合要求的方案数,其中 i = g i , 0 i = g_{i,0} i=gi,0 j = g i , 1 j = g_{i,1} j=gi,1

那么,根据最大权独立集的dp转移,该转移自然就出来了:

v v v 节点方案数为 f y , p , q f_{y,p,q} fy,p,q u u u 节点方案数为 f x , i , j f_{x,i,j} fx,i,j

对于 v v v 作为 u u u 的孩子,让 v v v 合并至 u u u 中,则
f x , i + m a x ( p , q ) , j + p ′ + f x , i , j × f y , p , q → f x , i + m a x ( p , q ) , j + p ′ 其中, f ′ 为 N e w   F , f 为 O l d   F f^{\prime}_{x,i + max(p,q),j + p} + f_{x,i,j} \times f_{y,p,q} \rightarrow f^{\prime}_{x,i + max(p,q),j + p} \\ 其中,f^{\prime} 为 New\ F,f 为 Old\ F fx,i+max(p,q),j+p+fx,i,j×fy,p,qfx,i+max(p,q),j+p其中,fNew F,fOld F

树上背包复杂度分析,鉴定为 O ( n 4 k 4 ) O(n^4k^4) O(n4k4),无法接受

我们发现题目中还有一个信息没有用上

k ≤ 5 k \leq 5 k5

这个 k k k 非常小,那么我们就要用这个支点,优化状态结构

怎么造出 k k k 的有利条件呢? 答案是放缩

我们设计的 g g g 函数条件太苛刻,状态太绝对了

如果读者有写过 P4719 【模板】“动态 DP”&动态树分治,就会想到 g i , 1 g_{i,1} gi,1 可以表示为 强制不选择本节点取得的最大权独立集,而 g i , 0 g_{i,0} gi,0不强制选择本节点取得的最大权独立集

这样,放缩就比较明显了,对于 g i , 0 g_{i,0} gi,0 g i , 1 g_{i,1} gi,1,中间缺什么呢?

我们知道,不强制选择肯定最优,强制不选择不一定优,

那么,容易得到强制不选择的答案一定无限趋近与不强制选择的答案,而其中 差值 ≤ v a l i 差值 \leq val_i 差值vali (因为强制不选择对于强制选择来说更灵活,所以与不强制选择相差不会超过 v a l i val_i vali

即:
0 ≤ g i , 0 − g i , 1 ≤ v a l i 0 \leq g_{i,0} - g_{i,1} \leq val_i 0gi,0gi,1vali

又因为, 每个节点的权值 ≤ k 每个节点的权值 \leq k 每个节点的权值k

所以
0 ≤ g i , 0 − g i , 1 ≤ v a l i ≤ k 0 \leq g_{i,0} - g_{i,1} \leq val_i \leq k 0gi,0gi,1valik

放缩结束!

那么,我们不用再枚举庞大的 d p dp dp 内存,只需要寻找 f i , 1 + i ( i = 0 ∼ k ) f_{i,1} + i (i = 0 \sim k) fi,1+i(i=0k) 的位置就可以了

此时自然得出 f u , v 1 , d f_{u,v_1,d} fu,v1,d 表示 u u u 子树中 g u , 0 , g u , 1 g_{u,0},g_{u,1} gu,0,gu,1 分别为 v 1 + d , v 1 v_1+d,v1 v1+d,v1 时的方案数,依然枚举 i , j , p , q i,j,p,q i,j,p,q,有转移:

f u , i + p + q , max ⁡ ( i + j + p , i + p + q ) − ( i + p + q ) ← f u , i + p + q , max ⁡ ( i + j + p , i + p + q ) − ( i + p + q ) + f u , i , j × f v , p , q f_{u,i+p+q,\max(i+j+p,i+p+q)-(i+p+q)} \gets f_{u,i+p+q,\max(i+j+p,i+p+q)-(i+p+q)}+ f_{u,i,j} \times f_{v,p,q} fu,i+p+q,max(i+j+p,i+p+q)(i+p+q)fu,i+p+q,max(i+j+p,i+p+q)(i+p+q)+fu,i,j×fv,p,q

tips: 为了对比,这里展示出原先的式子中变化:

i → ( i + j ) j → j p → ( p + q ) q → q 原式 = f x , i + m a x ( p , q ) , j + p ′ + f x , i , j × f y , p , q → f x , i + m a x ( p , q ) , j + p ′ 其中 f 的第 3 项含义变成了 f i , x , y − f j , p , q 的大小 i \rightarrow (i + j) \\ j \rightarrow j \\ p \rightarrow (p + q)\\ q \rightarrow q\\ 原式 = f^{\prime}_{x,i + max(p,q),j + p} + f_{x,i,j} \times f_{y,p,q} \rightarrow f^{\prime}_{x,i + max(p,q),j + p} \\ 其中 f 的 第3项含义变成了 f_{i,x,y} - f_{j,p,q} 的大小 i(i+j)jjp(p+q)qq原式=fx,i+max(p,q),j+p+fx,i,j×fy,p,qfx,i+max(p,q),j+p其中f的第3项含义变成了fi,x,yfj,p,q的大小

此题结束!

AC-code:

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

#define int long long

int rd() {
	int x = 0, w = 1;
	char ch = 0;
	while (ch < '0' || ch > '9') {
		if (ch == '-') w = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + (ch - '0');
		ch = getchar();
	}
	return x * w;
}

void wt(int x) {
	static int sta[35];
	int f = 1;
	if(x < 0) f = -1,x *= f;
	int top = 0;
	do {
		sta[top++] = x % 10, x /= 10;
	} while (x);
	if(f == -1) putchar('-');
	while (top) putchar(sta[--top] + 48);
}

const int N = 1005,mod = 1e9+7;

int n,k;

int head[N],nxt[N<<1],to[N<<1],cnt;

void init() {memset(head,-1,sizeof(head));}

void add(int u,int v) {
	nxt[cnt] = head[u];
	to[cnt] = v;
	head[u] = cnt++;
}

int siz[N],f[N][N * 5][6],g[N * 5][6];

inline void Add(int &a,int b){a += b;if(a >= mod) a -= mod;}

void dp(int x,int fa) {
	siz[x] = 1;
	for(int i = 1;i<=k;i++) f[x][0][i] = 1;
	for(int T = head[x];~T;T = nxt[T]) {
		int y = to[T];
		if(y == fa) continue;
		dp(y,x);
		memset(g,0,sizeof(g));
		for(int i = 0;i <= k * siz[x];i++) 
			for(int j = 0;j<=k;j++) 
				if(f[x][i][j])
					for(int p = 0;p <= k * siz[y];p++) 
						for(int q = 0;q <= k;q++)
							if(f[y][p][q])
								Add(g[i + p + q][max(i + j + p,i + p + q) - (i + p + q)],f[x][i][j] * f[y][p][q] % mod);
		memcpy(f[x],g,sizeof(g));
		siz[x] += siz[y];
	}
}

signed main() {
	init();
	n = rd(),k = rd();
	for(int i = 1,u,v;i<n;i++) {
		u = rd(),v = rd();
		add(u,v);
		add(v,u);
	}

	dp(1,0);

	for(int i = 1;i<=k * n;i++) {
		int ans = 0;
		for(int d = 0;d<=min(i,k);d++) Add(ans,f[1][i - d][d]);
		wt(ans),putchar('\n'); 
	}
	return 0;
}

回顾老题,典中典之新手恶梦

P2572 [SCOI2010] 序列操作

没什么好说的,慢慢实现就好了

关键是各功能之间的影响、关系要理清!

AC-code:

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

const int N = 1e5+5;

int n,m,a[N];

namespace sgm{
#define ls (p << 1)
#define rs (p << 1 | 1)
#define mid ((pl + pr) >> 1)

struct node{
	int l,r,lmax,rmax,mx,sum;
	node(int kl,int kr,int lm,int rm,int kmx,int s){
		l = kl,r = kr;
		lmax = lm,rmax = rm,mx = kmx,sum = s;
	} 
	node() {}
};


int L[N<<2],R[N<<2],mx[N<<2][2],cov[N<<2],lmax[N<<2][2],rmax[N<<2][2],sum[N<<2][2],rev[N<<2];

void newnode(int p,int pos) {
	L[p] = R[p] = a[pos];
	lmax[p][a[pos]] = rmax[p][a[pos]] = 1;
	mx[p][a[pos]] = sum[p][a[pos]] = 1;
}

void push_up(int p,int pl,int pr) {
	L[p] = L[ls],R[p] = R[rs];
	for(int i = 0;i<2;i++) {
		lmax[p][i] = lmax[ls][i];
		rmax[p][i] = rmax[rs][i];
		sum[p][i] = sum[ls][i] + sum[rs][i];
		mx[p][i] = max(mx[ls][i],mx[rs][i]);
		if(R[ls] == L[rs] && L[rs] == i) {
			if(sum[ls][i] == mid - pl + 1) lmax[p][i] += lmax[rs][i];
			if(sum[rs][i] == pr - mid) rmax[p][i] += rmax[ls][i];
			mx[p][i] = max(mx[p][i],lmax[rs][i] + rmax[ls][i]);
		}
	}
}

void build(int p,int pl,int pr) {
	cov[p] = -1,rev[p] = 0;
	if(pl == pr) {newnode(p,pl);return;}
	build(ls,pl,mid);
	build(rs,mid+1,pr);
	push_up(p,pl,pr);
}

void addCov(int p,int pl,int pr,int d){
	cov[p] = d;
	rev[p] = 0;
	lmax[p][d] = rmax[p][d] = mx[p][d] = pr - pl + 1;
	L[p] = R[p] = d;
	lmax[p][d ^ 1] = rmax[p][d ^ 1] = mx[p][d ^ 1] = 0;
	sum[p][d] = pr - pl + 1;
	sum[p][d ^ 1] = 0;
}

void addRev(int p,int pl,int pr) {
	if(cov[p] != -1) cov[p] ^= 1;
	rev[p] ^= 1;
	swap(lmax[p][0],lmax[p][1]);
	swap(rmax[p][0],rmax[p][1]);
	swap(mx[p][0],mx[p][1]);
	swap(sum[p][0],sum[p][1]);
	L[p] ^= 1;
	R[p] ^= 1;
}

void push_down(int p,int pl,int pr) {
	if(rev[p]) {
		addRev(ls,pl,mid);
		addRev(rs,mid+1,pr);
		rev[p] = 0;
	}
	if(cov[p] != -1) {
		addCov(ls,pl,mid,cov[p]);
		addCov(rs,mid+1,pr,cov[p]);
		cov[p] = -1;
	}
}

void Cov(int p,int pl,int pr,int l,int r,int d){
	if(l <= pl && pr <= r) {addCov(p,pl,pr,d);return;}
	push_down(p,pl,pr);
	if(l <= mid) Cov(ls,pl,mid,l,r,d);
	if(r > mid) Cov(rs,mid+1,pr,l,r,d);
	push_up(p,pl,pr);
}

void Rev(int p,int pl,int pr,int l,int r){
	if(l <= pl && pr <= r) {addRev(p,pl,pr);return;}
	push_down(p,pl,pr);
	if(l <= mid) Rev(ls,pl,mid,l,r);
	if(r > mid) Rev(rs,mid+1,pr,l,r);
	push_up(p,pl,pr);
}

int askSum(int p,int pl,int pr,int l,int r){
	if(l <= pl && pr <= r) {return sum[p][1];}
	push_down(p,pl,pr);
	int res = 0;
	if(l <= mid) res += askSum(ls,pl,mid,l,r);
	if(r > mid) res += askSum(rs,mid+1,pr,l,r);
	return res;
}

node cn(int p,int pl,int pr,int l,int r){
	if(l <= pl && pr <= r) {return node(L[p],R[p],lmax[p][1],rmax[p][1],mx[p][1],sum[p][1]);}
	push_down(p,pl,pr);
	if(l > mid) return cn(rs,mid+1,pr,l,r);
	else if(r <= mid) return cn(ls,pl,mid,l,r);
	else {
		node a = cn(ls,pl,mid,l,r),b = cn(rs,mid+1,pr,l,r),c;
		c.lmax = a.lmax,c.rmax = b.rmax;
		c.sum = a.sum + b.sum;
		c.mx = max(a.mx,b.mx);
		c.l = a.l,c.r = b.r;
		if(b.l == a.r && a.r == 1) {
			if(a.sum == mid - pl + 1) c.lmax += b.lmax;
			if(b.sum == pr - mid) c.rmax += a.rmax;
			c.mx = max(c.mx,a.rmax + b.lmax);
		}
		return c;
	}
}
}


signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr),cout.tie(nullptr);

	int opt,l,r;
	cin>>n>>m;
	for(int i = 1;i<=n;i++) cin>>a[i];
	sgm::build(1,1,n);
	while(m--) {
		cin>>opt>>l>>r;
		l++,r++;
		switch(opt) {
			case 0:
				sgm::Cov(1,1,n,l,r,0);
				break;
			case 1:
				sgm::Cov(1,1,n,l,r,1);
				break;
			case 2:
				sgm::Rev(1,1,n,l,r);
				break;
			case 3:
				cout<<sgm::askSum(1,1,n,l,r)<<'\n';
				break;
			case 4:
				cout<<sgm::cn(1,1,n,l,r).mx<<'\n';
				break;
		}
	}
	return 0;
}

2024/07/11

校内模拟赛 #2

A.弹钢琴

上一次写 d p dp dp 还是在上一次

最近几天,一直在写数据结构

d p dp dp 我是一点没练,成功在赛时精神不振,萎靡不振,做到了签到题耗时 3 h 3h 3h好成绩

这里附上我当天的运势
img

题面:

题目背景

PianoEater喜欢听钢琴曲,并且一直梦想着给他的GF Little Pink弹奏一曲。于是PianoEater去钢琴王国大学(Piano Kingdom University,简称PKU)找钢琴十级的rainbow学习弹琴。
PianoEater弹琴时,他的一只手上的5根手指不能交叉,并且两根手指不能放在同一个琴键上。同时,一只手的大拇指和小指之间最多间隔7个琴键。弹琴时,左右臂可以交叉,但是用(其中一只手的手指)去按(处于另一只手的两个手指之间)的按键是不允许的。

题目描述

现在PianoEater有一架有52个白键和36个黑键的钢琴,并且他要弹奏的曲子只需要按白键。在同一时刻,他只用弹奏一个音符。如果这个音符不移动大拇指就可以按到,那么他不需要耗费体力;否则他需要花费sqrt(x)(下取整)的体力来移动手的位置(也就是移动大拇指的位置)。其中x代表移动前后大拇指的位置之差的绝对值。

现在有一首由N个音符组成的乐曲,每个音符用0~51之间的一个整数表示,分别对应了52个白键。0是最左边的键,51是最右边的键。PianoEater想知道他弹完这首曲子最少需要耗费多少体力。

输入格式

输入的第一行是三个整数,L,R,N,分别表示初始时刻左手大拇指的位置、右手大拇指的位置和乐曲的音符数。

接下来N行每行一个在0~51之间的整数,代表需要弹奏的音符。

输出格式

输出一个整数,表示最少需要耗费的体力。

样例 #1
样例输入 #1
10 20 10
0
1
2
3
4
5
6
7
8
9
样例输出 #1
2
提示

【时间限制】

1s

【题目提示】

对于30%的数据,1<=N<=100

对于50%的数据,1<=N<=500

对于100%的数据,1<=N<=1000,4<=L<=51,0<=R<=47

作为一个 O I e r OIer OIer,我是没想到还要看题面背景,导致我直接错过了重要信息:

PianoEater弹琴时,他的一只手上的5根手指不能交叉,并且两根手指不能放在同一个琴键上。同时,一只手的大拇指和小指之间最多间隔7个琴键。弹琴时,左右臂可以交叉,但是用(其中一只手的手指)去按(处于另一只手的两个手指之间)的按键是不允许的。

警钟长鸣!

直接 d p dp dp 下一个音符,左右手就好了 dp[i][L][R]

因为用什么手指按目标琴键很关键,所以要枚举

右手大拇指位置 → [ x − 8 ∼ x ] 左手大拇指位置 → [ x ∼ x + 8 ] 右手大拇指位置 \rightarrow [x - 8 \sim x] \\ 左手大拇指位置 \rightarrow [x \sim x + 8] 右手大拇指位置[x8x]左手大拇指位置[xx+8]

然后转移式

l e f t − h a n d :   f i , x + l , k ← f i − 1 , j , k + ∣ j − ( x + l ) ∣ left-hand:\ f_{i,x + l,k} \leftarrow f_{i-1,j,k} + \sqrt{\lvert j - (x + l) \rvert} lefthand: fi,x+l,kfi1,j,k+j(x+l)∣

r i g h t − h a n d :   f i , j , x − l ← f i − 1 , j , k + ∣ ( x − l ) − k ∣ right-hand:\ f_{i,j,x - l} \leftarrow f_{i-1,j,k} + \sqrt{\lvert (x - l) - k \rvert} righthand: fi,j,xlfi1,j,k+∣(xl)k

注意题面背景,加上

左手条件:if(x + l < k || x + l - 5 > k + 5)
右手条件:if(j < x - l || x - l + 5 < j - 5)

就大功告成了

AC-code:

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

int rd() {
	int x = 0, w = 1;
	char ch = 0;
	while (ch < '0' || ch > '9') {
		if (ch == '-') w = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + (ch - '0');
		ch = getchar();
	}
	return x * w;
}

void wt(int x) {
	static int sta[35];
	int f = 1;
	if(x < 0) f = -1,x *= f;
	int top = 0;
	do {
		sta[top++] = x % 10, x /= 10;
	} while (x);
	if(f == -1) putchar('-');
	while (top) putchar(sta[--top] + 48);
}

const int N = 1e3 + 5,inf = 0x3f3f3f3f;

int dp[N][60][60];

signed main() {
	memset(dp,0x3f,sizeof(dp));
	int L = rd(),R = rd(),n = rd();
	dp[0][L][R] = 0;
	for(int i = 1;i<=n;i++) {
		int x = rd();
		for(int j = 0;j<=51;j++) 
			for(int k = 0;k <= 51;k++) if(dp[i-1][j][k] != inf){
				for(int l = 0;l<=8;l++) if(x + l < k || x + l - 5 > k + 5) dp[i][x + l][k] = min(dp[i][x + l][k],dp[i-1][j][k] + int(sqrt(abs(j - (x + l)))));
				for(int l = 0;l<=8;l++) if(j < x - l || x - l + 5 < j - 5) dp[i][j][x - l] = min(dp[i][j][x - l],dp[i-1][j][k] + int(sqrt(abs((x - l) - k)))); 
			}
	}

	int ans = inf;
	for(int i = 0;i<=51;i++) for(int j = 0;j<=51;j++) ans = min(ans,dp[n][i][j]);
	wt(ans);


	return 0;
}
B.捉迷藏

被骗了,这不是搜索

题面:

题目背景

vani和cl2在一片树林里捉迷藏……
这片树林里有N座房子,M条有向道路,组成了一张有向无环图。
树林里的树非常茂密,足以遮挡视线,但是沿着道路望去,却是视野开阔。如果从房子A沿着路走下去能够到达B,那么在A和B里的人是能够相互望见的。

题目描述

现在cl2要在这N座房子里选择K座作为藏身点,同时vani也专挑cl2作为藏身点的房子进去寻找,为了避免被vani看见,cl2要求这K个藏身点的任意两个之间都没有路径相连。
为了让vani更难找到自己,cl2想知道最多能选出多少个藏身点?

输入格式

第一行两个整数N,M。

接下来M行每行两个整数x、y,表示一条从x到y的有向道路。

输出格式

一个整数K,表示最多能选取的藏身点个数。

样例 #1
样例输入 #1
4 4
1 2
3 2
3 4
4 2
样例输出 #1
2
提示

【时间限制】

1s

【题目提示】

对于20% 的数据,N≤10,M<=20。

对于60% 的数据, N≤100,M<=1000。

对于100% 的数据,N≤200,M<=30000,1<=x,y<=N。

不难想到传递闭包,判断两点之间的连通性

但是接下来就不好办了,如果暴搜,只有 75 75 75 分,让人很不愉快

然而题解是 二分图最大匹配 → \rightarrow 最小点覆盖集

连通的两点 i , j i,j i,j i → j + N i \rightarrow j + N ij+N 建边 和 j + N → i j + N \rightarrow i j+Ni 建边

这样就可以让每一个连通的部分只有一个被加入答案

这题对于我这个没怎么写过二分图匹配的不大友好

img

找最小点覆盖集就可以了,方法是:

最小点覆盖集 = 二分图点数 − 二分图最大匹配 最小点覆盖集 = 二分图点数 - 二分图最大匹配 最小点覆盖集=二分图点数二分图最大匹配

AC-code:

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

int rd() {
	int x = 0, w = 1;
	char ch = 0;
	while (ch < '0' || ch > '9') {
		if (ch == '-') w = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + (ch - '0');
		ch = getchar();
	}
	return x * w;
}

void wt(int x) {
	static int sta[35];
	int f = 1;
	if(x < 0) f = -1,x *= f;
	int top = 0;
	do {
		sta[top++] = x % 10, x /= 10;
	} while (x);
	if(f == -1) putchar('-');
	while (top) putchar(sta[--top] + 48);
}

const int N = 205;

bool G[N][N];

vector<int> g[N << 1];

int mch[N << 1],vis[N<<1];

bool dfs(const int u,const int tag) {
	if(vis[u] == tag) return false;
	vis[u] = tag;
	for(int v:g[u]) if((mch[v] == 0)||dfs(mch[v],tag)){
		mch[v] = u;
		return true;
	}
	return false;
}

signed main() {

	int n = rd(),m = rd();
	for(int i = 1;i<=m;i++) {
		int u = rd(),v = rd();
		G[u][v] = 1;
	}

	for(int k = 1;k<=n;k++) 
		for(int i = 1;i<=n;i++) 
			for(int j = 1;j<=n;j++)
				G[i][j] = G[i][j] | (G[i][k] & G[k][j]);

	for(int i = 1;i<=n;i++) 
		for(int j = 1;j<=n;j++) 
			if(G[i][j]) {
				g[i].push_back(j + N);
				g[j + N].push_back(i);
			}
	
	int ans = 0;

	for(int i = 1;i<=n;i++) ans += dfs(i,i);

	wt(n - ans);

	return 0;
}
C.水叮当的舞步

或者是打开这个 UVA1505 Flood-it!

神仙 I D A ∗ IDA* IDA

估价函数为在场的剩余颜色种类

int F() {
	int t = 0;
	memset(f,0,sizeof(f));
	for(int i = 1;i<=n;i++) {
		for(int j = 1;j<=n;j++) {
			if(!f[m[i][j]] && v[i][j] ^ 1){
				f[m[i][j]] = 1;
				t++;
			}
		}
	}
	return t;
}

确定周围位置

void paint(int x,int y,int c){
	v[x][y] = 1;
	for(int i = 1;i<=4;i++) {
		int tx = x + d[i][0],ty = y + d[i][1];
		if((!check(tx,ty)) || v[tx][ty] == 1) continue;
		v[tx][ty] = 2;
		if(m[tx][ty] == c) paint(tx,ty,c);
	}
}

枚举每种颜色,以左上角为起点向周围染色,

int fill(int c) {
	int t = 0;
	for(int i = 1;i<=n;i++) {
		for(int j = 1;j<=n;j++) {
			if(m[i][j] == c && v[i][j] == 2) {
				t++;
				paint(i,j,c);
			}
		}
	}
	return t;
}

迭代加深搜索 AC-code:

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

int rd() {
	int x = 0, w = 1;
	char ch = 0;
	while (ch < '0' || ch > '9') {
		if (ch == '-') w = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + (ch - '0');
		ch = getchar();
	}
	return x * w;
}

void wt(int x) {
	static int sta[35];
	int f = 1;
	if(x < 0) f = -1,x *= f;
	int top = 0;
	do {
		sta[top++] = x % 10, x /= 10;
	} while (x);
	if(f == -1) putchar('-');
	while (top) putchar(sta[--top] + 48);
}

#define check(x,y) ((x) >= 1 && (x) <= n && (y) >= 1 && (y) <= n)

const int N = 10;
int n;
int d[5][2] = {{0,0},{0,1},{1,0},{-1,0},{0,-1}};
int m[N][N],v[N][N],f[N];

int F() {
	int t = 0;
	memset(f,0,sizeof(f));
	for(int i = 1;i<=n;i++) {
		for(int j = 1;j<=n;j++) {
			if(!f[m[i][j]] && v[i][j] ^ 1){
				f[m[i][j]] = 1;
				t++;
			}
		}
	}
	return t;
}

void paint(int x,int y,int c){
	v[x][y] = 1;
	for(int i = 1;i<=4;i++) {
		int tx = x + d[i][0],ty = y + d[i][1];
		if((!check(tx,ty)) || v[tx][ty] == 1) continue;
		v[tx][ty] = 2;
		if(m[tx][ty] == c) paint(tx,ty,c);
	}
}

int fill(int c) {
	int t = 0;
	for(int i = 1;i<=n;i++) {
		for(int j = 1;j<=n;j++) {
			if(m[i][j] == c && v[i][j] == 2) {
				t++;
				paint(i,j,c);
			}
		}
	}
	return t;
}

int rule;

bool dfs(int dep) {
	int g = F();
	if(dep + g > rule) return false;
	if(!g) return true;
	int rec[10][10];
	for(int i = 0;i<=5;i++) {
		memcpy(rec,v,sizeof(v));
		if(fill(i) && dfs(dep + 1)) return true;
		memcpy(v,rec,sizeof(rec));
	}
	return false;
}

signed main() {

	while(1){
		memset(v,0,sizeof(v));
		n = rd();
		if(n == 0) break;
		for(int i = 1;i<=n;i++) for(int j = 1;j<=n;j++) m[i][j] = rd();
		paint(1,1,m[1][1]);
		for(rule = 0;;rule++) if(dfs(0)) break;
		wt(rule);
		putchar('\n');
	}

	
	return 0;
}

P5664 [CSP-S2019] Emiya 家今天的饭

《深进》例题

题面:

题目描述

Emiya 是个擅长做菜的高中生,他共掌握 n n n烹饪方法,且会使用 m m m主要食材做菜。为了方便叙述,我们对烹饪方法从 1 ∼ n 1 \sim n 1n 编号,对主要食材从 1 ∼ m 1 \sim m 1m 编号。

Emiya 做的每道菜都将使用恰好一种烹饪方法与恰好一种主要食材。更具体地,Emiya 会做 a i , j a_{i,j} ai,j 道不同的使用烹饪方法 i i i 和主要食材 j j j 的菜( 1 ≤ i ≤ n 1 \leq i \leq n 1in 1 ≤ j ≤ m 1 \leq j \leq m 1jm),这也意味着 Emiya 总共会做 ∑ i = 1 n ∑ j = 1 m a i , j \sum\limits_{i=1}^{n} \sum\limits_{j=1}^{m} a_{i,j} i=1nj=1mai,j 道不同的菜。

Emiya 今天要准备一桌饭招待 Yazid 和 Rin 这对好朋友,然而三个人对菜的搭配有不同的要求,更具体地,对于一种包含 k k k 道菜的搭配方案而言:

  • Emiya 不会让大家饿肚子,所以将做至少一道菜,即 k ≥ 1 k \geq 1 k1
  • Rin 希望品尝不同烹饪方法做出的菜,因此她要求每道菜的烹饪方法互不相同
  • Yazid 不希望品尝太多同一食材做出的菜,因此他要求每种主要食材至多在一半的菜(即 ⌊ k 2 ⌋ \lfloor \frac{k}{2} \rfloor 2k 道菜)中被使用

这里的 ⌊ x ⌋ \lfloor x \rfloor x 为下取整函数,表示不超过 x x x 的最大整数。

这些要求难不倒 Emiya,但他想知道共有多少种不同的符合要求的搭配方案。两种方案不同,当且仅当存在至少一道菜在一种方案中出现,而不在另一种方案中出现。

Emiya 找到了你,请你帮他计算,你只需要告诉他符合所有要求的搭配方案数对质数 998 , 244 , 353 998,244,353 998,244,353 取模的结果。

输入格式

第 1 行两个用单个空格隔开的整数 n , m n,m n,m

第 2 行至第 n + 1 n + 1 n+1 行,每行 m m m 个用单个空格隔开的整数,其中第 i + 1 i + 1 i+1 行的 m m m 个数依次为 a i , 1 , a i , 2 , ⋯   , a i , m a_{i,1}, a_{i,2}, \cdots, a_{i,m} ai,1,ai,2,,ai,m

输出格式

仅一行一个整数,表示所求方案数对 998 , 244 , 353 998,244,353 998,244,353 取模的结果。

样例 #1
样例输入 #1
2 3 
1 0 1
0 1 1
样例输出 #1
3
样例 #2
样例输入 #2
3 3
1 2 3
4 5 0
6 0 0
样例输出 #2
190
样例 #3
样例输入 #3
5 5
1 0 0 1 1
0 1 0 1 0
1 1 1 1 0
1 0 1 0 1
0 1 1 0 1
样例输出 #3
742
提示

【样例 1 解释】

由于在这个样例中,对于每组 i , j i, j i,j,Emiya 都最多只会做一道菜,因此我们直接通过给出烹饪方法、主要食材的编号来描述一道菜。

符合要求的方案包括:

  • 做一道用烹饪方法 1、主要食材 1 的菜和一道用烹饪方法 2、主要食材 2 的菜
  • 做一道用烹饪方法 1、主要食材 1 的菜和一道用烹饪方法 2、主要食材 3 的菜
  • 做一道用烹饪方法 1、主要食材 3 的菜和一道用烹饪方法 2、主要食材 2 的菜

因此输出结果为 3   m o d   998 , 244 , 353 = 3 3 \bmod 998,244,353 = 3 3mod998,244,353=3。 需要注意的是,所有只包含一道菜的方案都是不符合要求的,因为唯一的主要食材在超过一半的菜中出现,这不满足 Yazid 的要求。

【样例 2 解释】

Emiya 必须至少做 2 道菜。

做 2 道菜的符合要求的方案数为 100。

做 3 道菜的符合要求的方案数为 90。

因此符合要求的方案数为 100 + 90 = 190。

【数据范围】

测试点编号 n = n= n= m = m= m= a i , j < a_{i,j}< ai,j<测试点编号 n = n= n= m = m= m= a i , j < a_{i,j}< ai,j<
1 1 1 2 2 2 2 2 2 2 2 2 7 7 7 10 10 10 2 2 2 1 0 3 10^3 103
2 2 2 2 2 2 3 3 3 2 2 2 8 8 8 10 10 10 3 3 3 1 0 3 10^3 103
3 3 3 5 5 5 2 2 2 2 2 2 9 ∼ 12 9\sim 12 912 40 40 40 2 2 2 1 0 3 10^3 103
4 4 4 5 5 5 3 3 3 2 2 2 13 ∼ 16 13\sim 16 1316 40 40 40 3 3 3 1 0 3 10^3 103
5 5 5 10 10 10 2 2 2 2 2 2 17 ∼ 21 17\sim 21 1721 40 40 40 500 500 500 1 0 3 10^3 103
6 6 6 10 10 10 3 3 3 2 2 2 22 ∼ 25 22\sim 25 2225 100 100 100 2 × 1 0 3 2\times 10^3 2×103 998244353 998244353 998244353

对于所有测试点,保证 1 ≤ n ≤ 100 1 \leq n \leq 100 1n100 1 ≤ m ≤ 2000 1 \leq m \leq 2000 1m2000 0 ≤ a i , j < 998 , 244 , 353 0 \leq a_{i,j} \lt 998,244,353 0ai,j<998,244,353

首先,我们不急着考虑全部 3 3 3 个要求,先试着考虑前两个要求,

对于任意一种烹饪方法 i i i,选择的菜数取值在 [ 0 ∼ ∑ j = 1 m a i , j ] [0 \sim \sum_{j = 1}^{m} a_{i,j}] [0j=1mai,j] 之中,共 1 + ∑ j = 1 m a i , j 1 + \sum_{j = 1}^{m} a_{i,j} 1+j=1mai,j 种选择。

对于整体,则有 ∏ i = 1 n ( 1 + ∑ j = 1 m a i , j ) \prod_{i = 1}^n (1 + \sum_{j = 1}^m a_{i,j}) i=1n(1+j=1mai,j) 中选择,但是我们不能让所有人饿肚子,故方案总数为
[ ∏ i = 1 n ( 1 + ∑ j = 1 m a i , j ) ] − 1 [\prod_{i = 1}^n (1 + \sum_{j = 1}^m a_{i,j})] - 1 [i=1n(1+j=1mai,j)]1

然后,割去不满足第 3 3 3 种情况的方案

我们设 f i , k , c f_{i,k,c} fi,k,c 为 前 i i i 种烹饪方式,选择了 k k k 种菜品,其中第 x x x 种菜品拿了 c c c 个的方案数

不难有转移:
f i , k , c ← [ j = x ] f i − 1 , j − 1 , c − 1 ⋅ a i , j f i , k , c ← [ j ≠ x ] f i − 1 , j − 1 , c ⋅ a i , j f i , k , c ← n o t h i n g   h a p p e n e d f i − 1 , j , c \large f_{i,k,c} \leftarrow^{[j = x]} f_{i-1,j-1,c-1} \cdot a_{i,j} \\ \large f_{i,k,c} \leftarrow^{[j \not = x]} f_{i-1,j-1,c} \cdot a_{i,j}\\ \large f_{i,k,c} \leftarrow^{nothing\ happened} f_{i-1,j,c} fi,k,c[j=x]fi1,j1,c1ai,jfi,k,c[j=x]fi1,j1,cai,jfi,k,cnothing happenedfi1,j,c

很遗憾,我们要枚举 i , k , c , x i,k,c,x i,k,c,x,完美的来到了 O ( m n 3 ) O(mn^3) O(mn3),不能接受

我们要开启人类智慧,放缩优化状态

我们有 c > ⌊ k 2 ⌋ , c c > \lfloor \frac{k}{2} \rfloor,c c>2k,c 是一个整数

那么,
c > ⌊ k 2 ⌋ ⇒ k ∈ o d d c > k 2 − 1 2 ⇒ 2 c + 1 > k c > \lfloor \frac{k}{2} \rfloor \Rightarrow^{k \in odd} c > \frac{k}{2} - \frac{1}{2} \Rightarrow 2c + 1 > k c>2kkoddc>2k212c+1>k

c > ⌊ k 2 ⌋ ⇒ k ∈ e v e n c > k 2 ⇒ 2 c > k c > \lfloor \frac{k}{2} \rfloor \Rightarrow^{k \in even} c > \frac{k}{2} \Rightarrow 2c > k c>2kkevenc>2k2c>k

k ∈ o d d k \in odd kodd 时,由于 2 c + 1 ∈ o d d 2c + 1 \in odd 2c+1odd,那么 2 c ≥ k + 1 2c \geq k + 1 2ck+1,所以 c > k 2 ⇒ 2 c > k c > \frac{k}{2} \Rightarrow 2c > k c>2k2c>k

所以, 2 c > k → 2 c − k > 0 2c > k \rightarrow 2c - k > 0 2c>k2ck>0

然后让我们变换一下,得到 c − ( k − c ) > 0 c - (k - c) > 0 c(kc)>0

这个式子的含义是不符合条件的方案必须满足 超过一半总数的菜品 − 剩下的菜品 > 0 超过一半总数的菜品 - 剩下的菜品 > 0 超过一半总数的菜品剩下的菜品>0(虽然很显然,但是还是用理论推导了一遍),

那么我们最后只要关注这个 Δ = 超过一半总数的菜品 − 剩下的菜品 > 0 \Delta = 超过一半总数的菜品 - 剩下的菜品 > 0 Δ=超过一半总数的菜品剩下的菜品>0 的方案数,累加即可得到所有不符合条件的方案数

那么,我们重新设 f i , Δ f_{i,\Delta} fi,Δ

有转移:

f i , Δ + 1 ← j = x a i , j ⋅ f i − 1 , Δ f_{i,\Delta + 1} \leftarrow^{j = x} a_{i,j} \cdot f_{i-1,\Delta} fi,Δ+1j=xai,jfi1,Δ
f i , Δ − 1 ← j ≠ x a i , j ⋅ f i − 1 , Δ f_{i,\Delta - 1} \leftarrow^{j \not= x} a_{i,j}\cdot f_{i-1,\Delta} fi,Δ1j=xai,jfi1,Δ

最后注意 Δ \Delta Δ 会是负数,扩大一倍去维护就好了

结束!

AC-code:

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

#define int long long

int rd() {
	int x = 0, w = 1;
	char ch = 0;
	while (ch < '0' || ch > '9') {
		if (ch == '-') w = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + (ch - '0');
		ch = getchar();
	}
	return x * w;
}

void wt(int x) {
	static int sta[35];
	int f = 1;
	if(x < 0) f = -1,x *= f;
	int top = 0;
	do {
		sta[top++] = x % 10, x /= 10;
	} while (x);
	if(f == -1) putchar('-');
	while (top) putchar(sta[--top] + 48);
}

const int N = 105,M = 2e3+5;

const int mod = 998244353;

int S[N],d[N][M],dp[N][N<<1];

signed main() {

	int n = rd(),m = rd();

	int prod = 1;
	for(int i = 1;i<=n;i++) {
		for(int j = 0;j<m;j++) {
			d[i][j] = rd();
			S[i] = (S[i] + d[i][j]) % mod;
		}
		prod = 1ll * prod * (S[i] + 1) % mod;
	}

	int ans = (prod + mod - 1) % mod;

	for(int x = 0;x < m;x++) {
		memset(dp,0,sizeof(dp));
		dp[0][0 + 101] = 1;
		int no = 0;
		for(int i = 1;i<=n;i++) {
			int re = (S[i] + mod - d[i][x]) % mod;
			for(int dt = 1;dt <= 201;dt++) {
				dp[i][dt + 1] = (dp[i][dt + 1] + 1ll * dp[i-1][dt] * d[i][x] % mod) % mod;
				dp[i][dt - 1] = (dp[i][dt - 1] + 1ll * dp[i-1][dt] * re % mod) % mod;
				dp[i][dt] = (dp[i][dt] + dp[i-1][dt]) % mod;
			}
		}
		for(int dt = 1;dt<=n;dt++) no = (no + dp[n][dt + 101]) % mod;
		ans = (ans + mod - no) % mod;
	}

	wt(ans);

	return 0;
}
  • 23
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值