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[136−13][s0]
状态转移:
若摸上来一张可以凑对的,则从 d p [ i − 1 ] [ j − 2 ] dp[i-1][j-2] dp[i−1][j−2] 转移来 ,概率为 3 j i \frac{3j}{i} i3j
若摸上来一张单的,则从 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j] 转移来,概率为 i − 3 j i \frac{i-3j}{i} ii−3j
#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 1≤x≤n,1≤y≤m 的整点。
有 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=kmy−1
用 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 1≤∑n≤2×105 , 1 ≤ ∑ m ≤ 5 × 1 0 5 1 ≤ ∑m ≤ 5 × 10^5 1≤∑m≤5×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;
}