20230819比赛

发布时间 2023-08-20 21:33:44作者: ZnPdCo

T1 无聊的草稿 GMOJ 1752

Description

  图中有N个点,每两点间只有唯一的路径,对于这样一个给定的图,最大的“毛毛虫”会有多大。毛毛虫包含一条主链,毛毛虫中的节点,要不在主链上,要么和主链上某节点相邻,如下图所示有两只合法的毛毛虫,点数越多,毛毛虫越大。
  
  img

Input

  输入文件第一行两个整数N,M(N≤1000000)
  接下来M行,每行两个整数a, b(a, b≤N)
  你可以假定没有一对相同的(a, b)会出现一次以上。

Output

  一个整数ans,表示最大的毛毛虫的大小。

Sample Input

5 4
1 2
1 3
4 1
5 1

Sample Output

5

Data Constraint

Hint

【数据规模】
  1. 对于20%的数据,N≤200
  2. 对于40%的数据,N≤5000
  3. 对于100%的数据,N≤10^6

比赛时一眼直径。可惜我不会……

下次一定把“树上问题”给看完。

然后我就在比赛时想到一个方法(好似就是用树形dp求直径)

下面定义 \(\text{child}\) 为儿子数量

定义 \(f[i]\) 表示以 \(i\) 为毛毛虫端点的最大毛毛虫大小,可以分为两种情况:

  1. 自己是毛毛虫起点

    \[f[i]=\text{child}+1 \]

  2. 自己继承儿子的毛毛虫

    \[f[i]=\max(child-1+1+f[j],f[i]) \]

    意思表示当前的毛毛虫大小为儿子的数量,减去继承的那个儿子的1,加上自己的1,加上继承那个儿子的子树大小。

然后就快乐的对拍啦!


然后还好我对拍了,我就发现上面的做法并不可行。

如果出现了:

graph TB 1 --> 2 --> 3 --> 4 3 --> 5 1 --> 6 --> 7 --> 8 6 --> 9

我们会发现,最好的毛毛虫是整棵树,但是在这里,我们计算出来的只有是1的两个子树中的毛毛虫大小,即5。

怎么解决呢?所以我定义了 \(g[i]\) 表示以 \(i\) 为承接点(例如上面的 \(1\))的最大毛毛虫大小。

这样理解可能会很困难,所以我可以这么理解:

  • \(f[i]\)\(i\) 为毛毛虫中一个节点,且 \(i\) 的父亲(根节点除外)也为毛毛虫的最大毛毛虫大小
  • \(g[i]\)\(i\) 为毛毛虫中一个节点,但 \(i\) 的父亲(根节点除外)不为毛毛虫的最大毛毛虫大小

附上 \(g\) 的计算方式:

\[g[i] = \text{两个最大的子树的 f 之和} + \text{child} + [i\not=1] - 2 + 1 \]

可能很难理解,我附上说明:意思是当前的毛毛虫大小为最大子树和次大子树大小之和,加上包括父亲(根节点除外)的周围节点数量,再减去两个子节点,再加上自己。

总结:

树上问题完全没有看完,遇到只能完蛋。

#include <cstdio>
#define ll long long
int n, m;
int ans;

int f[1000010];
int g[1000010];

int head[2000010];
int nxt[2000010];
int to[2000010], cnt;
void addEdge(int u, int v) {
    cnt++;
    to[cnt] = v;
    nxt[cnt] = head[u];
    head[u] = cnt;
}
inline int max(int x, int y) {
	return x>y?x:y;
}

void dfs(int u, int fa) {
	bool isFa = fa;
	int child = 0;
	for(int i = head[u]; i; i = nxt[i]) {
		int v = to[i];
		if(v != fa) child++;
		
	}


	// 方案1:把自己看作新链
	f[u] = child + 1;
	int mx1 = 0, mx2 = 0;
	for(int i = head[u]; i; i = nxt[i]) {
		// 方案2:选择儿子的一条新链
		int v = to[i];
		if(v == fa) continue;
		dfs(v, u);
		f[u] = max(f[u], child + f[v]);
		if(f[v] > mx1) {
			mx2 = mx1;
			mx1 = f[v];
		} else if(f[v] > mx2) {
			mx2 = f[v];
		}
	}
	// 方案3:选择两个大儿子配对
	if(mx1 && mx2) {
		//    自己  两个大儿子 包括父亲在内的相邻节点减去两个大儿子
		g[u] = 1 + mx1 + mx2 + (child + isFa) - 2;
	}
}

int main() {
    scanf("%d %d", &n, &m);
    for(int i = 1; i <= m; i++) {
        ll u, v;
        scanf("%lld %lld", &u, &v);
        addEdge(u, v);
        addEdge(v, u);
    }
	dfs(1,0);
	for(int i = 1; i <= n; i++) {
		ans = max(ans, max(f[i], g[i]));
		// printf("g[%lld]=%lld\n",i,g[i]);
	}
	printf("%d", ans);
}

T2 资源勘探 GMOJ 1282

Description

  教主要带领一群Orzer到一个雄奇地方勘察资源。
  这个地方可以用一个n×m的矩阵A[i, j]来描述,而教主所在的位置则是位于矩阵的第1行第1列。
  矩阵的每一个元素A[i, j]均为一个不超过n×m的正整数,描述了位于这个位置资源的类型为第A[i, j]类。教主准备选择一个子矩阵作为勘察的范围,矩阵的左上角即为教主所在的(1, 1)。若某类资源k在教主勘察的范围内恰好出现一次。或者说若教主选择了(x, y)即第x行第y列作为子矩阵的右下角,那么在这个子矩阵中只有一个A[i, j](1≤i≤x,1≤j≤y)满足A[i, j]=k,那么第k类资源则被教主认为是稀有资源。
  现在问题是,对于所有的(x, y),询问若(x, y)作为子矩阵的右下角,会有多少类不同的资源被教主认为是稀有资源。

Input

  输入的第一行包括两个正整数n和m,接下来n行,每行m个数字,描述了A[i, j]这个矩阵。

Output

  为了照顾Vijos脑残的输出问题,设B[i, j]表示仅包含前i行与前j列的子矩阵有多少个数字恰好出现一次,那么你所要输出所有B[i, j]之和mod 19900907。

Sample Input

2 3
1 2 3
3 1 2

Sample Output

10

Data Constraint

Hint

【样例说明】
  对于右下角为(1,1)的子矩阵,仅包含数字1,所以答案为1。
  对于右下角为(1,2)的子矩阵,数字1、2各出现一次,所以答案为2。
  对于右下角为(1,3)的子矩阵,数字1、2、3各出现一次,所以答案为3。
  对于右下角为(2,1)的子矩阵,数字1、3各出现一次,所以答案为2。
  对于右下角为(2,2)的子矩阵,数字2、3各出现一次,但是数字1出现了两次,所以数字1不统计入答案,答案为2。
  对于右下角为(2,3)的子矩阵,数字1、2、3均出现了两次,所以答案为0。
【数据说明】
  对于10%的数据,有N,M≤10;
  对于20%的数据,有N,M≤20;
  对于40%的数据,有N,M≤150;
  对于50%的数据,A[I, J]≤1000;
  对于70%的数据,有N,M≤800;
  对于100%的数据,有N,M≤1100,A[I, J] ≤N×M。

比赛时不会,现在会了也很蒙?

维护 \(b[j]\) 为第j列对答案的贡献。

那么贡献分为几类:

  1. 第一次出现,对答案贡献为 \(1\)
  2. 第二次出现,对答案贡献为 \(-1\)
  3. 第n>2次出现,对答案贡献为 \(0\)

我们分别考虑这三种情况,然后计算答案就行了。

总结:这种分别计算各元素对答案的贡献的思想不了解,导致本可以想到上面的(但是能不能打出来是另一码事)

#include <cstdio>
#define ll long long
#define P 19900907

ll a[1110][1110];								// QWQ
ll b[1110];										// b 储存某一列的贡献
ll v1[1110*1110];								// v1 储存最左出现的列数
ll v2[1110*1110];								// v2 储存第二左出现
ll n, m;
ll cnt, ans;
int main() {
	scanf("%lld %lld", &n, &m);
	for(ll i = 1; i <= n; i++) {
		for(ll j = 1; j <= m; j++) {
			scanf("%lld", &a[i][j]);
		}
	}
	for(ll i = 1; i <= n; i++) {
		cnt = 0;								// 更新寄存器
		for(ll j = 1; j <= m; j++) {
			if(!v1[a[i][j]]) {					// 如果这个点没访问过
				b[j]++;							// 更新该列贡献
				v1[a[i][j]] = j;				// 并更新最左侧的位置
			} else if(v1[a[i][j]] && v1[a[i][j]] <= j) {	// 如果这个位置访问过而且列数在子矩阵之内(对这次答案有贡献)
				if(v2[a[i][j]]) {				// 如果访问过第二次,说明这是第n>2次
					if(v2[a[i][j]] > j) {		// 判断这个第二次不是在子矩阵之内
						b[v2[a[i][j]]]++;		// 如果不是,贡献位置修改
						b[j]--;					// 因为不是第一或二次,贡献没用
						v2[a[i][j]] = j;		// 修改访问位置,确保v2最左
					}							// 判断这个第二次是在子矩阵之内,没关系,不用修改贡献
				} else {						// 判断没有访问过第二次,说明这是第二次
					b[j]--;						// 贡献没掉
					v2[a[i][j]] = j;			// 添加最左位置
				}
			} else if(v1[a[i][j]] && v1[a[i][j]] > j) {		// 如果这个位置访问过而且列数在子矩阵之外(对这次答案无贡献)
				b[j]++;							// 说明这次就要有贡献了
				b[v1[a[i][j]]] -= 2;			// 子矩阵之外的就是第二次了,所以需要从+1变成-1
				b[v2[a[i][j]]]++;				// 如果有第n>2次,那么它的-1在现在的第二次减过了,补回来
				v2[a[i][j]] = v1[a[i][j]];		// 更新
				v1[a[i][j]] = j;
			}
			(cnt += b[j]) %= P;
			(ans += cnt) %= P;
		}
	}

	printf("%lld", ans);
}

T3 排列统计 GMOJ 1283

Description

  对于给定的一个长度为n的序列{B[n]},问有多少个序列{A[n]}对于所有的i满足:A[1]~A[i]这i个数字中有恰好B[i]个数字小等于i。其中{A[n]}为1~n的一个排列,即1~n这n个数字在序列A[I]中恰好出现一次。
  数据保证了至少有一个排列满足B序列。

Input

  输入的第1行为一个正整数N,表示了序列的长度。
  第2行包含N个非负整数,描述了序列{B[i]}。

Output

  输出仅包括一个非负整数,即满足的{A[i]}序列个数。

Sample Input

3
0 1 3

Sample Output

3

Data Constraint

Hint

【样例说明】
  对于A序列为1~3的全排列分别对应的B序列如下(冒号左边为A序列,冒号右边为对应B的序列)
  1 2 3:1 2 3
  1 3 2:1 1 3
  2 1 3:0 2 3
  2 3 1:0 1 3
  3 1 2:0 1 3
  3 2 1:0 1 3
  所以有3个满足的A序列。
【数据说明】
  对于20%的数据,有N≤8;
  对于30%的数据,有N≤11且答案不大于20000;
  对于50%的数据,有N≤100;
  对于100%的数据,有N≤2000。

比赛考虑 dp,然后不会转移。

把题目转为矩阵就很明显了。

\(a[i]=j\) 表示为矩阵 \(A[i][j]=1\)

然后就是要求满足 \(A[1][1]\)\(A[i][i]\)\(b[i]\) 个 1 的矩阵个数。

这么一转换就很明显了。

因为矩阵的定义,一行一列不能有两个 1。这是很明显的。

假设我们填到:

1,0,?
0,1,?
?,?,?

需要补充上面的问号,就可以发现\(b[i]-b[i-1]\) 不能大于2,否则不能满足 一行一列不能有两个 1。

然后分类讨论dp即可。

这种转换题目的题考思维量,实在不会,就打个暴力吧……

#include <cstdio>
#include <cstring>
#include <algorithm>
#define ll long long
#define MX 100000000
using namespace std;
ll n;
ll a[2010];
ll b[2010];
struct node {
    ll a[100010], len;
    friend node operator *(const node &x, const int &y) {
        node c;
        memset(&c, 0, sizeof(c));
        c.len = x.len;
        for(ll i = 1; i <= c.len; i++) {
            c.a[i] += x.a[i] * y;
            if(c.a[i] >= MX) {
                c.a[i + 1] += c.a[i] / MX;
                c.a[i] %= MX;
                c.len = max(c.len, i + 1);
            }
        }
        for(ll i = c.len; i >= 1; i--) {
            if(c.a[i] == 0) c.len--;
            else break;
        }
        return c;
    }
} ans;
int main() {
    ans.a[1] = 1;
    ans.len = 1;
    scanf("%lld", &n);
    for(ll i = 1; i <= n; i++) {
        scanf("%lld", &b[i]);
        ll delta = b[i] - b[i - 1];
        if(delta == 0) {
            ans = ans * 1;
        } else if(delta == 1) {
            ans = ans * (2 * i - 1 - 2 * b[i - 1]);
        } else if(delta == 2) {
            ans = ans * (i - 1 - b[i - 1]) * (i - 1 - b[i - 1]);
        }
    }
    for(ll i = ans.len; i >= 1; i--) {
        if(i == ans.len) printf("%lld", ans.a[i]);
        else printf("%.8lld", ans.a[i]);
    }
}

T4 近海之主(treasure) GMOJ 6363

肯定是数学题啦,我怎么可能会。

题目的样例都没有懂,如果会应该可以多水几分。(事实最后输出样例交到T2导致T2爆零555)

几次比赛前有一道题:搞笑的代码,中提到“数学期望=sigma(概率*权值)”

当时没有在意,这道题样例其实就是概率乘以权值:

\[2\times\frac{40}{120}+3\times\frac{35}{120}+4\times\frac{30}{120}+5\times\frac{15}{120}=\frac{19}{6} \]

然后就可以暴力了(为什么比赛时忘记了)

然后正解其实很简单,一个环肯定无解,然后一个树的某一个点如果能被打开,肯定是要按照:

祖先-祖先的儿子-祖先的儿子的儿子-祖先的儿子的儿子的儿子-……-这个点的父亲-这个点

要一环扣一环,如果一步走错,那么它的某一个孙子的门就打不开了……

那么这些点走下去总共有 \(A_n^n\) 种情况。

那么一环扣一环刚好走到这个点的概率为:

\[\frac{1}{A_{dep}^{dep}}=\frac{1}{dep!} \]

所以枚举每一个点走下去就行了。

总结:概率是什么?为什么一直考概率?

我要恶补。

#include <cstdio>
#define P 998244353
#define ll long long
ll n;
ll a[100010];

ll head[100010];
ll nxt[100010];
ll to[100010], cnt;

ll ans;

ll inv[100010];
ll fact[100010];

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



void dfs(ll u, ll dep) {
    (ans += fact[dep]) %= P;
    for(ll i = head[u]; i; i = nxt[i]) {
        ll v = to[i];
        dfs(v, dep + 1);
    }
}
int main() {
    // freopen("treasure.in", "r", stdin);
    // freopen("treasure.out", "w", stdout);

    scanf("%lld", &n);

    inv[1] = 1;
    fact[1] = 1;
    for(ll i = 2; i <= n; i++) {
        inv[i] = (((P - P / i) * inv[P % i]) % P + P) % P;
        fact[i] = fact[i - 1] * inv[i] % P;
    }


    for(ll i = 1; i <= n; i++) {
        scanf("%lld", &a[i]);
        if(a[i] != -1) {
            addEdge(a[i], i);
        }
    }
    for(ll i = 1; i <= n; i++) {
        if(a[i] == -1) {
            dfs(i, 1);
        }
    }

    printf("%lld", ans);
}