2022牛客暑期多校训练营1

I(概率DP) C(枚举/几何) J(启发式合并)
I - Chiitoitsu

题意

初始手牌有 13 张麻将牌,相同牌至多出现 2 张。

每轮可以从牌堆摸牌,若达成七对子则自摸胡牌,若不然则选择手牌中某张牌并丢弃之。

给定初始手牌,求最优策略下达成七对子的期望轮数。

思路

最优策略 => 上帝视角(不会扔什么来什么)

即如果手上有一张单牌,那么另外三张一定还没被摸到。

d p [ i ] [ j ] dp[i][j] dp[i][j] 表示牌堆里还有 i i i 张牌,手上还有 j j j 张单牌时,还需要多少轮才能胡牌的期望。

设初始手牌中单牌数量为 s 0 s_0 s0,则答案为 d p [ 136 − 13 ] [ s 0 ] dp[136-13][s_0] dp[13613][s0]

状态转移:

若摸上来一张可以凑对的,则从 d p [ i − 1 ] [ j − 2 ] dp[i-1][j-2] dp[i1][j2] 转移来 ,概率为 3 j i \frac{3j}{i} i3j

若摸上来一张单的,则从 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j] 转移来,概率为 i − 3 j i \frac{i-3j}{i} ii3j

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

#define int long long
#define endl '\n'
const int N = 210, mod = 1e9 + 7;
int dp[N][N];

int ksm(int a, int b)
{
	int res = 1;
	while(b)
	{
		if(b & 1) res = a * res % mod;
		a = a * a % mod;
		b >>= 1;  
	}
	return res;
}
int inv(int x)
{
	return ksm(x, mod - 2);
}
void init()
{
	for(int i = 1; i <= 123; i++)
	{
		int v = inv(i);
//		cout << v << endl;
		for(int j = 0; j <= 13; j++)
		{
			if(j == 1) dp[i][j] = ((i - 3 * j) * v % mod * (dp[i - 1][j] + 1) % mod + 3 * j * v % mod) % mod;
			else dp[i][j] = ((i - 3 * j) * v % mod * (dp[i - 1][j] + 1) % mod + (dp[i - 1][j - 2] + 1) * 3 * j % mod * v % mod) % mod;
		}
	}
	return;
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	int T;
	init();
	cin >> T;
	for(int i = 1; i <= T; i++)
	{
		string s;
		cin >> s;
		map<string, int> m;
		int dui = 0;
		for(int i = 0; i < s.length() - 1; i += 2)
		{
			string t = "??";
			t[0] = s[i], t[1] = s[i + 1];
			if(m[t]) dui++;
			else m[t]++;
		}
		int s0 = 13 - 2 * dui;
		cout << "Case #" << i << ": " << dp[123][s0] << endl;
	}
	return 0; 
}

记忆化搜索

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

#define int long long
const int N = 210, mod = 1e9 + 7;
int dp[N][N];

int ksm(int a, int b)
{
	int res = 1;
	while(b)
	{
		if(b & 1) res = a * res % mod;
		a = a * a % mod;
		b >>= 1;  
	}
	return res;
}
int inv(int x)
{
	return ksm(x, mod - 2);
}

int dfs(int i, int j)
{
	if(dp[i][j] != -1) return dp[i][j];
	
	if(j == 0) return 0;
	int v = inv(i);
	int p1 = (i - 3 * j) * v % mod;
	int p2 = 3 * j * v % mod;
	
	if(j == 1) dp[i][j] = (p1 * (dfs(i - 1, j) + 1) % mod + p2) % mod;
	else dp[i][j] = (p1 * (dfs(i - 1, j) + 1) % mod + p2 * (dfs(i - 1, j - 2) + 1) % mod) % mod;
	return dp[i][j];
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	memset(dp, -1, sizeof dp);
	int T;
	cin >> T;
	for(int i = 1; i <= T; i++)
	{
		string s;
		cin >> s;
		map<string, int> m;
		int dui = 0;
		for(int i = 0; i < s.length() - 1; i += 2)
		{
			string t = "??";
			t[0] = s[i], t[1] = s[i + 1];
			if(m[t]) dui++;
			else m[t]++;
		}
		int s0 = 13 - 2 * dui;
		cout << "Case #" << i << ": " << dfs(123, s0) << endl;
	}
	return 0; 
}
C - Grab the Seat!

题意

二维平面,屏幕是 ( 0 , 1 ) – ( 0 , m ) (0, 1)–(0, m) (0,1)(0,m) 的线段。

n n n m m m 列座位在屏幕前面,是坐标范围 1 ≤ x ≤ n , 1 ≤ y ≤ m 1 ≤ x ≤ n, 1 ≤ y ≤ m 1xn,1ym 的整点。

k k k 个座位已经有人,求出到屏幕的视线不被任何人挡住的座位数量。

q q q 次询问,每次修改一个人的坐标后求出答案。

思路

只有200次询问,每次询问都暴力求一遍。

对于一个人能遮住的范围如下图阴影部分,

在这里插入图片描述

考虑把这个夹角的两条边分开讨论,先看斜率为正的部分,容易发现,斜率越大遮盖的越多。

从低到高枚举每一行,如果斜率大于之前的最大斜率,则只需要考虑最新的这条直线的覆盖情况,否则这条新直线不会遮盖到新的区域。

对于每一行,通过当前最大斜率可以计算出对应的 x x x 坐标,

直线 y = k m x + 1 y=k_mx+1 y=kmx+1 ,则 x = y − 1 k m x=\frac{y-1}{k_m} x=kmy1

x x x 坐标更新这一行的答案,这里减去一个无限小是防止这条直线刚好遮挡到那个点,那么答案要减去1,int数组向下取整

double k = 0;
for(int i = 1; i <= m; i++)
{
	k = max(k, 1.0 * (i - 1) / p[i]);
	if(k == 0) up[i] = p[i] - 1;
	else up[i] = 1.0 * (i - 1) / k - 1e-9;
}

在这里插入图片描述

斜率为负的部分同理

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define int long long
typedef pair<int,int> PII;
typedef long long LL;
const int N = 250010;
int n, m, k, q;
int X[N], Y[N], p[N]; // p 是这一行的第一个人 
int up[N], down[N];

void f()
{
	fill(p + 1, p + 1 + m, n + 1);
	for(int i = 1; i <= k; i++)	p[Y[i]] = min(p[Y[i]], X[i]);

	
	double k = 0;
	for(int i = 1; i <= m; i++)
	{
		k = max(k, 1.0 * (i - 1) / p[i]);
		if(k == 0) up[i] = p[i] - 1;
		else up[i] = 1.0 * (i - 1) / k - 1e-9;
	}
	
	k = 0;
	for(int i = m; i >= 1; i--)
	{
		k = min(k, 1.0 * (i - m) / p[i]);
		if(k == 0) down[i] = p[i] - 1;
		else down[i] = 1.0 * (i - m) / k - 1e-9;
	}
	

	int res = 0;
	for(int i = 1; i <= m; i++)
	{
		res += min({n, up[i], down[i]});
	}
	cout << res << endl;
	return;
}
void solve()
{
	cin >> n >> m >> k >> q;
	for(int i = 1; i <= k; i++) cin >> X[i] >> Y[i];
	for(int i = 1; i <= q; i++)
	{
		int p, x, y;
		cin >> p >> x >> y;
		X[p] = x, Y[p] = y;
		f();
	}
	return;
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	int T = 1;
//	cin >> T;
	while(T--)
	{
		solve();
	}
	return 0;
}
J - Serval and Essay

题意

有一张 n 个点 m 条边的无重边无自环的有向图。

初始时可以选择一个点染黑,其余点均为白点。

若某个点所有入边的起点均为黑点,则该点可以被染黑。

最大化图中黑点数量。

多组数据, 1 ≤ ∑ n ≤ 2 × 1 0 5 1 ≤ ∑n ≤ 2 × 10^5 1n2×105 , 1 ≤ ∑ m ≤ 5 × 1 0 5 1 ≤ ∑m ≤ 5 × 10^5 1m5×105

思路

考虑通过 “A 确定 B” 这种关系来将 A 与 B 合并,即只要 A 变黑了, B 就一定会黑,那么就没有必要考虑从 B 出发,染 A 是更优的。

当 A 和 B 合并后,B 所有的出边就可以合并到 A 的出边中去,考虑启发式合并,每次都将出边少的合并到出边大的中,用set维护可以自动去重。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
//#define int long long
typedef pair<int,int> PII;
const int N = 200010;
int n;
int fa[N], siz[N];
set<int> to[N], from[N];
int find(int x)
{
	if(x == fa[x]) return x;
	return fa[x] = find(fa[x]);
}
void merge(int x, int y)
{
	x = find(x), y = find(y);
	if(x == y) return;
	if(to[x].size() < to[y].size()) swap(x, y); 
	// 将出边少的合并到出边大的 把y合并到x里 
	fa[y] = x;
	siz[x] += siz[y];
	vector<PII> mg;
	for(auto t : to[y])
	{
		to[x].insert(t);
		from[t].erase(y); 
		from[t].insert(x);
		if(from[t].size() == 1) mg.push_back({t, x});
	}
	for(auto [x, y] : mg) merge(x, y);
}
void solve()
{
	cin >> n;
	for(int i = 1; i <= n; i++) fa[i] = i, siz[i] = 1;
	for(int i = 1; i <= n; i++)
	{
		int k;
		cin >> k;
		for(int j = 1; j <= k; j++)
		{
			int y;
			cin >> y;
			to[y].insert(i);
			from[i].insert(y);
		}
	}
	for(int i = 1; i <= n; i++)
	{
		if(from[i].size() == 1)
			merge(*from[i].begin(), i);
	}
	int res = 0;
	for(int i = 1; i <= n; i++)
	{
		res = max(res, siz[i]);
	}
	cout << res << endl;
	for(int i = 1; i <= n; i++)
	{
		to[i].clear(), from[i].clear();
	}
	return;
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	int T = 1;
	cin >> T;
	for(int i = 1; i <= T; i++)
	{
		cout << "Case #" << i << ": ";
		solve();
	}
	return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值