//https://img2018.cnblogs.com/blog/1646268/201908/1646268-20190806114008215-138720377.jpg

2023.8.31解题报告

  • @Author: Aisaka_Taiga
  • @Date: 2023-08-31 15:37:31
  • @LastEditTime: 2023-08-31 15:48:02
  • @LastEditors: Aisaka_Taiga
  • @FilePath: \Desktop\2023.8.31解题报告.md
  • 心比天高,命比纸薄。

2023.8.31 解题报告

T1 比较简单,考试的时候 1h 左右就切了,T2 推了半天没看出来是个变异的杨辉三角,T3 打了最低档的暴力,挂了20分,T4 大模拟没想下手,直接判的无解输出的 -1,没分。

T1

期望得分: 100

实际得分: 100

简单的 DP。

我们考虑设 \(f[i][j][1/0]\) 表示当前枚举到第 \(i\) 个数,选了 \(j\) 个数当山谷点,且 \(1\) 表示第 \(i\) 个点被选为山谷点,反之则为 \(0\)

我们枚举 \(i,j\),考虑如何转移,我们发现连续的两个点是肯定不能都是山谷点,所以有以下三种转移:

  • 当前点不选为山谷点,\(f[i][j][0] = max(f[i-1][j][1], f[i-1][j][0])\)
  • 当前点本来就是山谷点,可以选为山谷点,\(f[i][j][1] = f[i - 1][j][0] + a[i]\)
  • 当前点本来不是山谷点,我们要把他变成山谷点,\(f[i][j][1] = f[i - 1][j - 1][0] + min(a[i - 1], a[i + 1]) - 1\)

然后在枚举的过程中取 \(\max\) 就好了。

/*
 * @Author: Aisaka_Taiga
 * @Date: 2023-08-31 08:09:11
 * @LastEditTime: 2023-08-31 11:12:52
 * @LastEditors: Aisaka_Taiga
 * @FilePath: \Desktop\张潇涵\T1.cpp
 * 心比天高,命比纸薄。
 */
#include <bits/stdc++.h>

#define int long long
#define N 2100

using namespace std;

inline int read(){int x=0,f=1;char ch=getchar();while(!isdigit(ch)){f=ch!='-';ch=getchar();}while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return f?x:-x;}

int n, a[N], m, ans, f[N][N][2];

signed main()
{
    freopen("t1.in", "r", stdin);
    freopen("t1.out", "w", stdout);
	n = read(), m = read();
	for(int i = 1; i <= n; i ++) a[i] = read();
	for(int i = 2; i <= n - 1; i ++)
	{
		for(int j = 0; j <= m; j ++)
		{
			f[i][j][0] = max(f[i - 1][j][1], f[i - 1][j][0]);
            if(a[i] < a[i - 1] && a[i] < a[i + 1]) f[i][j][1] = f[i - 1][j][0] + a[i];
			else if(j) f[i][j][1] = f[i - 1][j - 1][0] + min(a[i - 1], a[i + 1]) - 1;
            ans = max(max(f[i][j][1], f[i][j][0]), ans);
		}
	}
	cout << ans << endl;
	return 0;
}
/*
3 1
5 6 7

5 5
2 7 8 9 2

5 1
8 7 6 7 9
*/

T2

预估得分:40

实际得分:40

考试的时候肝了一个多小时,没看出来是个杨辉三角。

我们先假设一开始给的都是 \(1, 2, 3,\dots ,n\)

然后我们从上往下推第一项是由那些加减得到的,我是选的 \(n = 7\)

然后我们能得到下面的一些序列:

1 0 -3 0 3 0 -1
1 1 -2 -2 1 1
1 0 -2 0 1
1 1 -1 -1
1 0 -1
1 1
1

我们把它倒过来:

1
1 1
1 0 -1
1 1 -1 -1
1 0 -2 0 1
1 1 -2 -2 1 1
1 0 -3 0 3 0 -1

我们可以发现每两行对应了杨辉三角的一行,也就是说,我们求得的序列里面,第 \(i\) 行对应的是杨辉三角的第 \((i + 1) / 2\) 行。

但是我们发现其实有一些地方是不一样的,比如,偶数行的地方,每一个杨辉三角的值都是重复了一次,然后得到的,而且前两列系数为正,后两列系数为负,再后两列为正,再后两列为负……

对于奇数行,我们看到里面的第偶数列都是 \(0\),然后就是正常的杨辉三角,系数的正负同上。

写个程序验证一下我们的猜想,然后就可以写代码了。

/*
 * @Author: Aisaka_Taiga
 * @Date: 2023-08-31 14:22:35
 * @LastEditTime: 2023-08-31 15:27:40
 * @LastEditors: Aisaka_Taiga
 * @FilePath: \Desktop\T2.cpp
 * 心比天高,命比纸薄。
 */
#include <bits/stdc++.h>

#define int long long
#define P 1000000007
#define N 200010

using namespace std;

int F[N], Finv[N], inv[N];

inline void init()
{
    inv[1] = 1;
    for(int i = 2; i < N; i ++)//线性求逆元
        inv[i] =(P - P / i) * 1ll * inv[P % i] % P;
    F[0] = Finv[0] = 1;
    for(int i = 1; i < N; i ++)
	{
        F[i] = F[i - 1] * 1ll * i % P;//求阶乘
        Finv[i] = Finv[i - 1] * 1ll * inv[i] % P;//求阶乘的逆元
    }
}

inline int C(int n, int m)
{
    if(m < 0 || m > n) return 0;//特判方案为零的情况
    return F[n] * 1ll * Finv[n - m] % P * Finv[m] % P;
}

inline int cal(int n, int m)
{
	if(n & 1 && !(m & 1)) return 0;//如果n是奇数,第偶数个的项的系数为0
	m = (m + 1) / 2;//计算求的组合数的参数
	n = (n + 1) / 2;
	int now = C(n - 1, m - 1);//系数为C(n-1,m-1)
	if(m % 2 == 0) return now * -1;//如果m是偶数就取反
	return now;//返回系数
}

signed main()
{
	init();
	int n, a, ans = 0;
	cin >> n;
	for(int i = 1; i <= n; i++)
	{
		cin >> a;
		ans = (ans + a * cal(n, i)) % P;//计算系数,相乘求和
	}
	cout << (ans + P) % P << endl;
	return 0;
}
/*
10
1 2 3 4 5 6 7 8 9 10

1
1 1
1 0 -1
1 1 -1 -1
1 0 -2 0 1
1 1 -2 -2 1 1
1 0 -3 0 3 0 -1
*/

T3

期望得分:20

实际得分:0

T3 有点难了啊。。。

我们先考虑 $k = 1 $ 的情况。

在这种情况下,我们只需要让叶子节点 \(1\) 到根节点的路径上所有的点都被已经收买的人占了,且满足这些点都是其子树的最大值。

将被收买的人按实力值从小到大排序。设 \(f[i][j]\)
表示已经处理了前 \(i\) 个被收买的人,\(j\) 的二进制的每一位表示这个位置上是否有被收买的人占据。那么转移式如下

\[f[i][j+2^k]+=f[i][j]\times C_{a_{i}-j-2}^{2^k-1}\times (2^k)! \]

其中 \(k\)\(j\) 的二进制位中为 \(0\) 的位。

\(C_{a_{i} - j -2}^{2^k-1}\) 表示在实力在比 \(a_{i}\) 小的没有选过且不为 \(1\)\(a_{i}-j-2\) 个选手中选 \(2^k-1\)(因为已经确定了要有 \(a_{i}\) ,所以要减 \(1\))个来组成 \(k\) 位置的子树。

因为子树内部可以任意排序,所以要乘上 \((2^k)!\)

输出答案时,因为 \(1\) 的位置任意,所以要乘上 \(2^n\)

再来考虑 \(k\ge 1\) 的情况。

我们可以考虑对 LIS(最长上升子序列)的维护方法。

从小到大依次来维护每个数字的 LIS,这个数字的 LIS 等于在其之前的所有数字的 LIS 的最大值 \(+1\)

我们可以暴力求出所有经过 LIS 过程后最大的 LIS 值能够 \(\ge k\) 的状态。状态的数量并不大,\(n=9\) 的时候才不到 \(120000\)。用这些状态来当之前状压的状态,这样即可求出答案。

改的标程。

#include<bits/stdc++.h>

#define int long long
#define N 120010

using namespace std;

inline int read(){int x=0,f=1;char ch=getchar();while(!isdigit(ch)){f=ch!='-';ch=getchar();}while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return f?x:-x;}

int n, m, k, tot = 0, t1 = 0, a[25], cnt[N], mod;
int ans = 0, yh[1005][1005], jc[1005], f[25][N];
string pt[N], to[N];
map<string, int> z, re;

inline void dfs(string s, int now)//now是已经填了的数的个数
{
	if(!z[s]) z[s] = 1, pt[++ tot] = s;//如果当前的状态未出现过,标记,存下来
	else return;//出现过了就没必要搜了
	if(now == n) return;//如果到了n就直接退出
	char c = '0';
	for(int i = 0; i < n; i ++)//遍历s的每一个字符
	{
		if(s[i] == '0')//如果要是当前字符没填
		{
			s[i] = c + 1;//填上
            // cout << c << endl;
			dfs(s, now + 1);//继续往下搜
			s[i] = '0';//回溯
		}
		else c = max(c, s[i]);//否则就给c取较大值
	}//保证是最长上升子序列
}

inline void dd()
{
	for(int i = 1; i <= tot; i ++)//枚举所有状态
	{
		string s = pt[i];//取出状态串
		char c = '0';
		for(int j = 0; j < n; j ++)//填串
		{
			if(s[j] == '0') s[j] = c + 1, c ++;//等于0就填当前的c
			else c = max(c, s[j]);//否则就更新c的值
		}
		if(c >= '0' + k) to[++ t1] = pt[i], re[pt[i]] = t1;//如果要是当前c大于k了,就是可以的方案存进to,re标记在to中的序号
	}
	for(int i = 1; i <= t1; i ++)//枚举所有可行的状态
		for(int j = 0; j < n; j ++)//枚举每一位
			if(to[i][j] != '0') cnt[i] |= (1 << j);//cnt存下当前位置是否有值
	return ;
}

inline void init()
{
	yh[0][0] = 1;
	for(int i = 1; i <= 1000; i ++)
	{
		yh[i][0] = yh[i][i] = 1;//处理组合数
		for(int j = 1; j < i; j ++) yh[i][j] = (yh[i - 1][j - 1] + yh[i - 1][j]) % mod;
	}
	jc[0] = 1;
	for(int i = 1; i <= 1000; i ++) jc[i] = jc[i - 1] * i % mod;//处理阶乘
	return ;
}

inline int ksm(int x, int y)//快速幂
{
	int res = 1;
	while(y)
	{
		if(y % 2 == 1) res = (res * x) % mod;
		x = (x * x) % mod;
		y >>= 1;
	}
	return res % mod;
}

inline void gt(string &s, int t)//找到下一个该是多少
{
	char c = '0';
	for(int i = 0; i < t; i ++) c = max(c, s[i]);
	s[t] = c + 1;
}

signed main()
{
	n = read(), m = read(), k = read(), mod = read();
	for(int i = 1; i <= m; i ++) a[i] = read();
	sort(a + 1, a + m + 1);//? 将输入的战力从小到大排序。
	string s;
	for(int i = 1; i <= n; i ++) s = s + '0';//一开始全是0
	dfs(s, 0);//从当前的状态开始搜索1号叶子节点到根节点的LIS
	dd();//处理cnt
	init();//处理组合数,阶乘
	f[0][1] = 1;//初始值
	for(int i = 1; i <= m; i ++)//枚举每一个被收买的人。
	{
		for(int j = 1; j <= t1; j ++)//枚举每一个合法状态
		{
			if(f[i - 1][j])//如果前一个有值
			{
				f[i][j] = (f[i][j] + f[i - 1][j]) % mod;//先加上前面的
				string now = to[j], nxt;//取出状态,nxt是下一个
				for(int t = 0; t < n; t ++)//枚举每一个字符
				{
					if(now[t] == '0' && a[i] >= cnt[j] + (1 << t) + 1)//当前点没有填人,子树可以填满
					{
						nxt = now;
						gt(nxt, t);//转移。
						f[i][re[nxt]] = (f[i][re[nxt]] + f[i - 1][j] * yh[a[i] - cnt[j] - 2][(1 << t) - 1] % mod * jc[1 << t] % mod) % mod;
					}
				}
			}
		}
	}
	for(int i = 1; i <= t1; i ++)
		if(cnt[i] == (1 << n) - 1)
			ans = (ans + f[m][i]) % mod;
	ans = ans * ksm(2, n) % mod;//1的位置任意,乘2^n
	cout << ans << endl;
	return 0;
}
posted @ 2023-08-31 22:03  北烛青澜  阅读(13)  评论(0编辑  收藏  举报