1002
数学 贪心 博弈
世界杯赌球,每个队伍都可以能否获得冠军,不同队伍之间押注相互独立。一共w元资金,第iii个队伍压赢的赔率xix_ixi,压输的赔率是xi/(xi−1)x_i/(x_i-1)xi/(xi−1)。问能收回的资金,最坏情况下的最大值是多少。
大体策略
首先考虑压输还是压赢。如果一部分队伍压赢,另一部分队伍压输。最坏情况下,可以这样思考,庄家如果能操纵比赛,可以控制任意一个队伍获得冠军,问怎么控制比赛能让我们收回的资金最小?
- 如果某个我们认为赢的队伍赢了,我们压输的队伍全都压对了,资金都能收回,并且有一个我们压赢的队伍也能收回资金
- 如果某个我们认为输的队伍赢了,我们压赢的队伍全都收不回资金了,然后压输的队伍只有那个赢的收不回资金,别的都能收回。
- 如果是一个我们没有压赢也没有压输的队伍夺冠了,压赢的队伍钱全收不回资金,压输的队伍全都能收回资金。
显然三种情况我们的收益是满足严格的大小关系:第一种情况>第三种情况>第二种情况。那庄家一定会做局形成第二种局面,这也就是我们要考虑的最坏情况,庄家会在我们压输的队伍中选一个夺冠。
从这个角度考虑,我们如果买了队伍输,就不该买队伍赢,因为庄家一定从输的队伍里选一个让他夺冠,买赢的部分是没有收益的。更进一步,这些买输的队伍资金该如何分配?庄家肯定会选那个收益最大的队伍,让他夺冠,这样我们赚的最少。一个队伍的收益就是押注资金 * 赔率,那么为了让收益的最小值最大化,我们应该让各个队伍的押注资金 * 赔率尽量均分,这样不论哪个被庄家选中我们剩余队伍的收益都不会太少。
注意还有另一种情况,我们可以压所有队伍赢,这样不论哪个队伍夺冠,都能或者这个队伍的收益。同理,由于庄家可以操控哪个队伍夺冠,我们应该让资金在考虑赔率加权的意义上均匀分配,使得不管哪个队伍夺冠收益都相等。
资金分配
下面来思考资金具体怎么分布,先考虑简单情况,压所有队伍赢。此时我们需要让每个队伍获胜后收益相同。
这也就是,对于赔率xix_ixi的队伍,押注资金应该和赔率成反比,押注量为
wi=1xi∑j=1n1xj⋅ww_i = \frac{\frac{1}{x_i}}{\sum_{j=1}^n \frac{1}{x_j}} \cdot wwi=∑j=1nxj1xi1⋅w
这样任意一队,压中了的收益都为
ri=w∑j=1n1xjr_i = \frac{w}{\sum_{j=1}^n \frac{1}{x_j}} ri=∑j=1nxj1w
另一个情况是,只压部分队伍输,类似地也需要让每个队伍压中后的收益均等,于是每个队伍压中的收益类似前面的公式,只是赔率从xix_ixi变成xi/(xi−1)x_i/(x_i-1)xi/(xi−1)。并且最坏情况是夺冠队伍在我们压输的队伍中,那只是这个夺冠队伍的收益拿不到,其他压输的队伍都有收益,根据赔率排序,枚举一个前缀压输,设有k个队伍,那么能拿到k-1个队伍的收益。
具体实现如下,开始的w/sumw/sumw/sum就是压所有队赢的收益,后面排序枚举前缀就是压一部分队输的收益。
c
void solve() {
int n, w;
cin >> n >> w;
db sum = 0;
vi a(n);
rep(i, 0, n - 1) {
cin >> a[i];
sum += 1.0 / a[i];
}
db ans = w / sum;
sort(a.begin(), a.end());
db s1 = 0;
rep(i, 1, n) {
db t = a[i - 1] / (a[i - 1] - 1.0);
s1 += 1 / t;
if (i >= 2) {
ans = max(ans, w * (i - 1) / s1);
}
}
cout << fixed << setprecision(10) << ans << '\n';
}
1003
DP 状态设计
一个序列a中选一个子序列b,b满足每个元素都不小于左侧元素的和。问b序列最长多少。
注意到b序列的规则,如果把不小于变成等号,且只等于前两个元素,就是bi=bi−1+bi−2b_i=b_{i-1}+b_{i-2}bi=bi−1+bi−2,这就是斐波那契数列,而斐波那契的增长已经很快了,增长到VVV的项数只有O(logV)O(\log V)O(logV),这个序列只会更快,因此b序列最大长度不会太长,对于V=1e18V=1e18V=1e18也只有不到log21e18=60\log_21e18=60log21e18=60项。
考虑暴力搜索怎么做,需要dfs枚举子序列,状态里需要包含当前枚举到第几个元素,当前的序列长度,以及序列元素和,用来决定下一个元素能不能选。考虑转成DP的话,把长度和元素和放在DP状态里,注意元素和是很大的,状态放不下,同时DP值的维度只返回bool表示一个状态能不能到达,太浪费,考虑把元素和放在dp值维度,序列长度仍然可以放在状态维度。
也就是f(i,j)f(i,j)f(i,j)表示考虑a的前i个元素,b序列选中长度为j的元素和信息,为了让b序列尽量长,前缀和需要尽量小,于是维护的是最小的前缀元素和。
转移类似背包,枚举一个元素选或不选,选的时候还有额外条件:aia_iai必须不小于∑bj\sum b_j∑bj才能选。
最后检查f(n,i)f(n,i)f(n,i),一个状态的dp值不为初始值则表示这个长度是可以取到的,更新最大长度。注意由于元素值V=1e18V=1e18V=1e18,初始值inf需要更大,这里设8e18,其实大于1e18就可以,2e18就行。
c
void solve() {
int n;
cin >> n;
vi a(n + 1);
rep(i, 1, n) {
cin >> a[i];
}
int len = 100;
vvi f(n + 1, vi(len + 1, inf));
rep(i, 0, n) {
f[i][0] = 0;
}
rep(i, 1, n) {
rep(j, 1, len) {
f[i][j] = f[i - 1][j];
if (a[i] >= f[i - 1][j - 1]) {
f[i][j] = min(f[i][j], f[i - 1][j - 1] + a[i]);
}
}
}
int ans = 0;
rep(i, 1, len) {
if (f[n][i] < inf) {
ans = max(ans, i);
}
}
cout << ans << '\n';
}
1004
费用流
n个人m种玩具,每个人喜欢x种玩具,最后必须获得这x种里的一种。每种玩具可以买多个,随着买的变多单价会增加,也就是wijw_{ij}wij表示第i种玩具,第j个的价格,满足wij>=wi,j−1w_{ij}>=w_{i,j-1}wij>=wi,j−1
问让n个人都得到喜欢的玩具的最小代价?
比较朴素的想法是考虑贪心或者dp,但是玩具种类太多了,m=1e3。如果一个问题看起来像贪心优化问题,可能还带反悔,但是用贪心做的复杂度太高,那么往往可以转化成网络流。网络流能自动实现带反悔的贪心
考虑建图,
- 每个人只能获得一个玩具,因此源点到每个人流量1,费用0
- 每个人有x种喜欢的玩具,向这x个玩具连边,流量1,费用0。这看起来会导致一个人能选多个玩具,但我们前面规定了源点到每个人流量1,所以每个人只有一次选择机会。由于保证∑x=1e4\sum x=1e4∑x=1e4,因此这层的总边数只有1e4,可以接受
- 每个玩具随着买的变多有不同的价格,假设玩具i有y个价格,向汇点连y跳边,第j条的流量1,费用wi,jw_{i,j}wi,j。这样网络流会自动对于每个玩具选价格最小的几个。
在这张图上跑费用流,最终肯定能流满,也就是最大流是n,每个人都能得到玩具,此时最小费用就是买玩具的最小代价。其中可能出现的贪心,反悔,都在网络流模板中帮我们处理了,dinic网络流自带反向边,支持反悔。
实现上需要注意是,
- 这个板子点数是全局变量,默认的是点数就是n,但是这题的点数实际上是n+m+2,因此需要手动把spfa里的所有点初始化循环范围改成n+m+2,或者用另一个全局变量N=n+m+2表示点数。
- 并且这题多测,需要清空head,cur,cnt,mxf,cost,d和vis会在spfa内清空,不用管。
- 此外从复杂度分析,使用当前弧优化(也就是前向星枚举不从head开始,而是维护一个cur表示当前点的临接表上次遍历到哪里了),dinic最坏复杂度O(n2m)O(n^2m)O(n2m),n,m分别是点数,边数。
- 在所有边容量都是1的前提下,理论复杂度可以降低到O(mmin(m1/2,2/3))O(m\min(m^{1/2},^{2/3}))O(mmin(m1/2,2/3)),对于这题的数据来说完全能过了。
- 并且更进一步,如果这是个二分图,也就是每个点入度=1或出度=1,则可以进一步降低到O(mn)O(m\sqrt n)O(mn )。本题接近二分图,但不完全满足这个条件,虽然人和玩具可以二分图的左部和右部,且人这一边,和源点连边都只有一条,入度都是1,但是玩具这边和汇点连边,每个点可能有多条边,出度不为1。
- 如果不加当前弧优化,复杂度可能退化到接近EK算法的O(nm2)O(nm^2)O(nm2),本题数据很强,删掉当前弧优化会超时。
c
#include<bits/stdc++.h>
using namespace std;
const int N = 5e3 + 10;
#define ll long long
#define db double
#define inf 0x3f3f3f3f
#define rep(i,x,y) for(int i=(x);i<=(y);i++)
#define pll pair<int,int>
#define int ll
int n, m, s, t, head[N * 20], cnt = 1, cur[N], cost, d[N], mxf;
bool vis[N];
int rd(void) {
int x = 0, f = 1;
char s;
s = getchar();
while (s > '9' || s < '0') {
if (s == '-')f = -1;
s = getchar();
}
while (s <= '9' && s >= '0') {
x = x * 10 + s - '0';
s = getchar();
}
x *= f;
return x;
}
struct edge {
int v, next, w, c;
} e[N * 20];
void add_(int u, int v, int w, int c = 0) {
e[++cnt] = (edge) {
v, head[u], w, c
};
head[u] = cnt;
}
void add(int u, int v, int w, int c = 0) {
add_(u, v, w, c);
add_(v, u, 0, -c);
}
bool spfa(void) {
rep(i, 1, n + m + 2)d[i] = inf, vis[i] = 0;
d[t] = 0;
vis[t] = 1;
deque<int>q;
q.emplace_back(t);
while (!q.empty()) {
int u = q.front();
q.pop_front();
vis[u] = 0;
for (int i = head[u]; i; i = e[i].next) {
int v = e[i].v, c = e[i ^ 1].c, w = e[i ^ 1].w;
if (w && d[v] > d[u] + c) {
d[v] = d[u] + c;
//cout<<u<<' '<<v<<' '<<d[v]<<endl;
if (!vis[v]) {
vis[v] = 1;
if (!q.empty() && d[v] < d[q.front()])q.emplace_front(v);
else q.emplace_back(v);
}
}
}
}
return d[s] != inf;
}
int dfs(int u, int flow) {
if (u == t) {
vis[t] = 1;
return flow;
}
int used = 0;
vis[u] = 1;
for (int &i = cur[u]; i && used < flow; i = e[i].next) {
int v = e[i].v, c = e[i].c, w = e[i].w;
if (!vis[v] && w && d[u] == d[v] + c) {
int k = dfs(v, min(w, flow - used));
if (k) {
e[i].w -= k;
e[i ^ 1].w += k;
cost += c * k;
used += k;
}
}
}
vis[u] = 0;
return used;
}
void solve() {
mxf = cost = 0;
cnt = 1;
rep(i, 1, (n + m + 2) * 20) {
head[i] = cur[i] = 0;
}
cin >> n >> m;
s = n + m + 1, t = n + m + 2;
rep(i, 1, n) {
int x;
cin >> x;
rep(j, 1, x) {
int v;
cin >> v;
add(i, n + v, 1, 0);
}
add(s, i, 1, 0);
}
rep(i, 1, m) {
int y;
cin >> y;
rep(j, 1, y) {
int w;
cin >> w;
add(n + i, t, 1, w);
}
}
while (spfa()) {
vis[t] = 1;
rep(i, 1, n + m + 2) {
cur[i] = head[i];
}
while (vis[t]) {
memset(vis, 0, sizeof vis);
mxf += dfs(s, inf);
}
}
// cout << mxf << ' ' << cost;
cout << cost << '\n';
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int test = 1;
cin >> test;
while (test--) {
solve();
}
}
1005
分解质因子 观察
每轮选两个a,b,要求a×b=xa\times b=xa×b=x,max(a,b)>1\max(a,b)>1max(a,b)>1,然后令x=gcd(a,b)x=\gcd(a,b)x=gcd(a,b)。问最多可以进行几轮操作。
考虑初始x的质因数分解,每个质因数的指数每轮操作拆成两半分给ab,然后gcd时取min,想让他减小的尽量慢,每轮都应该尽量均分。那么最慢的情况就是一直均分,一直除2看几轮后变成0
以上逻辑对于每个质因子都成立,最后是所有质因子指数都变0了才结束。那么为了让部分轮数尽量多,考虑初始指数最大的那个质因子,计算他一直除2,变成0需要几轮
x很大,分解质因子最简单的方法是pollard rho。
但注意本题我们并不需要知道具体分解的质因子是那些,只要知道质因子的最大指数。
那么可以用另一个更简单的做法:筛出不超过1e6的质数,先枚举这些质数进行分解,从x里除掉,枚举完之后,剩下的x要么是一个大质数,要么是两个大质数的乘积(因为如果是三个大于1e6的质数相乘,就超过1e18了)。
如果是两个大质数乘积,且这两个大质数不同,或剩下的是一个质数,他们的指数都是1,如果是两个相同质数乘积,可以通过x×x=x\sqrt x \times\sqrt x =xx ×x =x来判断,这个质因子的指数是2,不论怎么样,指数都是可以快速确定的。
pollard rho做法
c
const ll P = 2305843009213693951LL;
// 基础大数工具
ll qpow(ll a, ll b, ll mod = P) {
ll res = 1;
a %= mod;
while (b > 0) {
if (b & 1) res = (ll)((int128)res * a % mod);
a = (ll)((int128)a * a % mod);
b >>= 1;
}
return res;
}
// Miller-Rabin 素数测试
bool miller_rabin(ll n, ll a) {
if (a % n == 0) return true;
ll d = n - 1;
while (d % 2 == 0) d /= 2;
ll x = qpow(a, d, n);
if (x == 1 || x == n - 1) return true;
while (d != n - 1) {
x = (ll)((int128)x * x % n);
d *= 2;
if (x == n - 1) return true;
if (x == 1) return false;
}
return false;
}
bool is_prime(ll n) {
if (n < 2) return false;
if (n == 2 || n == 3) return true;
static const ll bases[] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37};
for (ll a : bases) {
if (n == a) return true;
if (!miller_rabin(n, a)) return false;
}
return true;
}
// Pollard's Rho 质因数分解
ll gcd(ll a, ll b) {
return b == 0 ? a : gcd(b, a % b);
}
ll pollard_rho(ll n) {
if (n % 2 == 0) return 2;
if (is_prime(n)) return n;
ll x = rand() % (n - 2) + 2, y = x, c = rand() % (n - 1) + 1, d = 1;
while (d == 1) {
x = (ll)(((int128)x * x + c) % n);
y = (ll)(((int128)y * y + c) % n);
y = (ll)(((int128)y * y + c) % n);
d = gcd(abs(x - y), n);
if (d == n) return pollard_rho(n);
}
return d;
}
map<ll, int> factors;
void get_factors(ll n) {
if (n == 1) return;
if (is_prime(n)) {
factors[n]++;
return;
}
ll d = pollard_rho(n);
get_factors(d);
get_factors(n / d);
}
void solve() {
int x;
cin >> x;
factors.clear();
get_factors(x);
int mx = 0;
for (auto &[k, v] : factors) {
mx = max(mx, v);
}
int ans = 0;
while (mx) {
mx /= 2;
ans++;
}
cout << ans << '\n';
}
枚举1e6内质数做法
c
void solve() {
int x;
cin >> x;
unordered_map<int, int>mp;
for (int y : primes) {
while (x % y == 0) {
x /= y;
mp[y]++;
}
}
int mx = 1;
for (auto &[k, v] : mp) {
mx = max(mx, v);
}
if (x != 1) {
int t = sqrt(x);
if (t * t == x) {
mx = max(mx, 2ll);
}
}
int ans = 0;
while (mx) {
mx /= 2;
ans++;
}
cout<<ans<<'\n';
}
1006
数论 结论 贪心
在1,n1,n1,n里选一个子集,两两互不整除。使得子集最大,并且在此基础上字典序最小。
一个结论是,最大子集的大小一定是n/2n/2n/2上取整,一个构造方法是n/2,nn/2,nn/2,n全选。对于如何证明这是最大的,一个比较显然的思路是,如果选了任意一个小于n/2的数,n/2,nn/2,nn/2,n里就有一个它的倍数不能选了,也就是这个基础上怎么调整都只会更小。另一个严谨的证明是,考虑每个数可以表示为m2xm2^xm2x,其中mmm是奇数,一个m形成的所有数是一条链,这条链上两两都能整除,那么一条链在集合里只能存在一个数。因此集合最大大小就等于链数,而这样的链在1,n1,n1,n内有n/2条,对于每条链,选链上不超过n的最大元素加入集合。
这题还要求字典序最小,那么就是在n/2,nn/2,nn/2,n的基础上,做一些微调,让字典序更小。具体过程可以这样考虑:对于一个数x,枚举它的全部倍数,计算它的全部倍数,当前在集合内的个数,如果选了x,x所有的倍数都不能在集合内了,那么未来最大化集合大小,只有在集合内x的倍数恰好为1时,才让考虑x加入集合,把那个倍数从集合中删除,并且为了字典序最小,从大到小枚举x,那么x一定比它的倍数更小,替换后字典序更小。
c
void solve() {
int n;
cin >> n;
vi vis(n + 1);
rep1(i, n, 1) {
int cnt = 0;
for (int j = 2 * i; j <= n; j += i) {
cnt += (vis[j] == 0);
}
if (cnt == 0) {
continue;
} else if (cnt == 1) {
vis[2 * i] = 1;
} else {
vis[i] = 1;
}
}
int ans = 0;
rep(i, 1, n) {
if (!vis[i]) {
ans++;
}
}
cout << ans << '\n';
rep(i, 1, n) {
if (!vis[i]) {
cout << i << ' ';
}
}
cout << '\n';
}
1009
思维 图论
n个题n个选项一一对应。现在只能确定到第i题答案一定是ai,bia_i,b_iai,bi之一,问使用最优策略时,答对题数的期望是多少,或者说通过题数的期望最大是多少
把这个问题建模转化一下,每个题的两个选项ai,bia_i,b_iai,bi连边的话,那么转化成一张无向图上,每条边都要确定一个方向,并且每个点入度必须都为1,或者看成无向图的话,就是每条边要选中一个端点,每个端点必须只被一条边选中。
对于这张图,n条边n个点,是个基环树,每个叶子只有一条临边,这条边必须指向叶子,然后把确定的边剥离,又产生了新的叶子,这样下去基环树的树枝部分每条边的选法都是确定的。对于环上,则有两种选法,具体来说随机选一条环上边,有两个方向可选,一旦一条边确定方向,剩余边也都确定了,那么实际上只有两种选法。
对于树枝上点,选法是完全确定的,答案也是确定的,选的肯定都是对的,因此每个点对期望的贡献都是1;对于环上点,只有两种选法,要么都对,要么都错,因此每个点的期望都是1×12+1×0=121\times\frac12+1\times 0=\frac121×21+1×0=21。
c
void solve() {
int n;
cin >> n;
vvi g(n + 1);
vi in(n + 1);
rep(i, 1, n) {
int a, b;
cin >> a >> b;
g[a].push_back(b);
g[b].push_back(a);
in[a]++;
in[b]++;
}
int cnt = 0;
queue<int>q;
rep(i, 1, n) {
if (in[i] == 1) {
q.push(i);
}
}
while (q.size()) {
int u = q.front();
q.pop();
cnt++;
for (int v : g[u]) {
if (--in[v] == 1) {
q.push(v);
}
}
}
int ans = (n - cnt) * inv(2, M2) % M2;
ans = (ans + cnt) % M2;
cout << ans << '\n';
}
1010
期望 数学变形
w种音符,一个人喜欢的旋律是一个长度l的随机子串。现在随机生成一个序列,每次生成一个音符,直到出现喜欢的旋律。问生成序列的长度期望是多少?
类似于一个无穷级数求和,如果1开头长度为l的子串就是喜欢的,概率为p=1/wlp=1/w^lp=1/wl,如果是2开头的子串是喜欢的,那么说明1开头的不是喜欢的旋律,这是个条件概率,(1−p)×q(1-p)\times q(1−p)×q,其中q是在1开头不是喜欢旋律的条件下,2开头是喜欢的概率,以此类推,有无穷项条件概率,分别乘上序列长度,就是答案E,这个形式求不了,考虑问题变形。
对于无穷级数一个经典结论是,从第1项开始和从第i项开始,求和可能是一样的,或者是相差一个系数。对于本题,随机生成的无穷长的序列,从1开始看,出现喜欢旋律的期望长度,应该等于从2开始看的期望长度。于是有
E=p×1+(1−p)×(E+1) E=p\times 1+(1-p)\times (E+1) E=p×1+(1−p)×(E+1)
含义是从1开始的期望,如果1开头长度l的序列就是喜欢的,概率是ppp,长度是1,如果1开头长度w的序列不是喜欢的,归约到子问题:2开头的序列的期望长度是多少,答案也是E,并且由于是2开头了,前面有第一项,因此期望还要+1
这是个关于E的方程,解出来就是E=wl+l−1E=w^l+l-1E=wl+l−1。这题还有别的思路,但都更复杂,这个类似错位相减法的思路就是处理无穷级数比较简单的分析思路了
c
void solve() {
int w, l;
cin >> w >> l;
int ans = qpow(w, l, M2);
ans = (ans + l - 1 + M2) % M2;
cout << ans << '\n';
}