3.31考试总结
A
题面:51nod 1850
题解:概率DP
话说为什么背包对这些神仙们这么简单,直接带过去的啊
首先我们来看一下\(O(n^4)\)的做法
首先枚举人,枚举他抽到的牌\(O(n^2)\)
然后用背包转移\(O(n^2)\)
由于是用背包转移,所以要先对每个人的卡\(sort\)一遍
背包求出有\(k\)个人的卡比他大的概率
转移方程
其中\(prob\)为\(k\)这个人比他大的概率
初值\(f[0]=1,f[1...n]=0\)
表示\(i\)这个人选中\(j\)这张牌的概率为\(\frac{p[i][j]}{sum[i]}\),有\(k\)个人比他大的概率是\(f[k]\),获得的收益是\(v[k+1]*(1-\frac{g[i][j]}{100})\),然后求和
#include<bits/stdc++.h>
using namespace std;
inline void read(int& x)
{
x = 0; char c = getchar();
while (!isdigit(c)) c = getchar();
while (isdigit(c)) x = x * 10 + c - '0', c = getchar();
}
#define maxn 205
const int P = 1e9 + 7;
int v[maxn], num[maxn], inv[maxn], sum[maxn];
int ans[maxn], f[maxn], pos[maxn], cost[maxn];
struct Person
{
int a, g, p;
bool operator < (const Person& q) const { return a > q.a; }
}p[maxn][maxn];
void exgcd(int a, int b, int& x, int& y)
{
if (!b) { x = 1, y = 0; return; }
exgcd(b, a % b, y, x);
y -= a / b * x;
}
inline int getinv(int a)
{
int x, y;
exgcd(a, P, x, y);
x %= P;
return x > 0 ? x : x + P;
}
int main()
{
int n; read(n);
for (int i = 1; i <= n; ++i)
{
read(num[i]);
for (int j = 1; j <= num[i]; ++j)
read(p[i][j].a), read(p[i][j].g), read(p[i][j].p), sum[i] += p[i][j].p;
sort(p[i] + 1, p[i] + num[i] + 1);//从大到小排序
inv[i] = getinv(sum[i]);
}
for (int i = 1; i <= n; ++i) read(v[i]);
int base = getinv(100);
for (int i = 1; i <= n; ++i)
{
for (int j = 1; j <= n; ++j) pos[j] = 1, cost[j] = 0;
for (int j = 1; j <= num[i]; ++j)
{
f[0] = 1;
for (int k = 1; k <= n; ++k) f[k] = 0;
for (int k = 1; k <= n; ++k)
{
if (i == k) continue;
while (pos[k] <= num[k] && p[k][pos[k]].a > p[i][j].a)
cost[k] += p[k][pos[k]].p, ++pos[k];//求出k这个人比他大的概率之和
for (int tp, l = n; l >= 0; --l)
{
tp = 1ll * f[l] * (sum[k] - cost[k]) % P * inv[k] % P;
if (l) tp = (0ll + tp + 1ll * f[l - 1] * cost[k] % P * inv[k] % P) % P;
f[l] = tp;
}
}
for (int k = 0; k < n; ++k)
ans[i] = (1ll * ans[i] + 1ll * f[k] * (100 - p[i][j].g) % P * base % P * v[k + 1] % P * inv[i] % P * p[i][j].p % P) % P;
}
}
for (int i = 1; i <= n; ++i) printf("%d\n", ans[i]);
return 0;
}
然后看正解\(O(n^3)\)
对于一张卡,只有大于它的会对它的排名产生影响
将值\(A\)在\(n\)个人中的期望排名写成生成函数的形式
如果大于\(A\)就会让其排名增大\(1\),所以令\(p_i\)为第\(i\)个人的选择大于\(A\)的概率
对于一个人的选择\(A_i\),就直接把此时的多项式除掉第\(i\)个人的式子
算出期望排名之后,直接乘上概率和收益,累加进答案
由于这个多项式只有两项,所以可以暴力乘除
所以把所有卡按权值排序
每次移动,只会有一个多项式被改变
我们就维护各项系数,除掉这一项,算完贡献,再乘回去
然后我的做法是设\(g\)表示原多项式,\(f\)表示除掉了这一项的多项式
所以有$$g_x=f_{x-1}p_i+f_x(1-p_i)$$
初值\(g[0]=1,g[1...n-1]=0\)
这个很显然
然后逆推的话$$f_x=\frac{g_x-p_if_{x-1}}{1-p_i}$$
#include<bits/stdc++.h>
using namespace std;
template<typename T>
inline void read(T& x)
{
x = 0; char c = getchar();
while (!isdigit(c)) c = getchar();
while (isdigit(c)) x = x * 10 + c - '0', c = getchar();
}
#define maxn 200005
#define P 1000000007
inline void exgcd(int a, int b, int& x, int& y)
{
if (!b) { x = 1, y = 0; return; }
exgcd(b, a % b, y, x);
y -= a / b * x;
}
inline int getinv(int a)
{
int x, y;
exgcd(a, P, x, y);
x %= P;
return (x < 0) ? x + P : x;
}
int inv[maxn];
inline void init()//线性求逆元
{
inv[1] = 1;
for (int i = 2; i <= 200000; ++i) inv[i] = (0ll + P - 1ll * P / i * inv[P % i]) % P;
//我一开始直接写了个阶乘逆元上去,然后调了半天……第一次写这种
}
struct Card
{
int a, g, p, id;
bool operator < (const Card& q) const { return a < q.a; }
}c[maxn];
int n, tot, v[maxn], sum[maxn], now[maxn], f[maxn], g[maxn], ans[maxn];
//now[x]就是目前的prob[x]
inline void insert(int x)//乘进去
{
int prob = 1ll * now[x] * inv[sum[x]] % P, prob2 = (1 - prob + P) % P;
g[0] = 1ll * f[0] * prob2 % P;
for (int i = 1; i < n; ++i) g[i] = (1ll * f[i - 1] * prob + 1ll * f[i] * prob2) % P
}
inline void delet(int x)//除掉
{
int prob = 1ll * now[x] * inv[sum[x]] % P, invprob = 1ll * inv[sum[x] - now[x]] * sum[x] % P;
f[0] = 1ll * g[0] * invprob % P;
for (int i = 1; i < n; ++i) f[i] = (0ll + g[i] - 1ll * f[i - 1] * prob % P + P) * invprob % P;
}
int main()
{
init();
read(n);
for (int i = 1, m; i <= n; ++i)
{
read(m);
while (m--)
{
read(c[++tot].a), read(c[tot].g), read(c[tot].p), c[tot].id = i;
sum[i] += c[tot].p;
c[tot].g = 1ll * (100 - c[tot].g) * inv[100] % P;
}
}
sort(c + 1, c + tot + 1);
for (int i = 1; i <= n; ++i) read(v[i]);
g[0] = 1;
for (int i = 1; i <= tot; ++i)
{
delet(c[i].id);
for (int j = 0; j < n; ++j)
ans[c[i].id] = (ans[c[i].id] + 1ll * f[j] * v[n - j] % P * c[i].g % P * c[i].p % P * inv[sum[c[i].id]] % P) % P;
now[c[i].id] += c[i].p;
insert(c[i].id);
}
for (int i = 1; i <= n; ++i) printf("%d\n", ans[i] < 0 ? ans[i] + P : ans[i]);
return 0;
}
B 异或约数和
题面:51nod 1984
题解:数学
考虑一个数\(i\),它会被算进贡献里\(\left \lfloor \frac{n}{i} \right \rfloor\)次
所以我们可以数论分块,如果余数为奇数就异或上这个区间
通过打表可以发现
复杂度:\(O(\sqrt n)\)
#include<bits/stdc++.h>
using namespace std;
template<typename T>
inline void read(T& x)
{
x = 0; char c = getchar();
while (!isdigit(c)) c = getchar();
while (isdigit(c)) x = x * 10 + c - '0', c = getchar();
}
long long n, ans;
inline long long get(long long x)
{
long long tp = x - ((x >> 2) << 2);
if (tp == 1) return 1;
if (tp == 2) return x + 1;
if (tp == 3) return 0;
return x;
}
int main()
{
read(n);
for (register long long l = 1, r, i; l <= n; l = r + 1)
{
r = n / (n / l);
if ((n / l) & 1) ans ^= get(r) ^ get(l - 1);
}
printf("%lld\n", ans);
return 0;
}
C 小朋友的笑话
题面:51nod 2014
题解:线段树+\(set\)维护区间
考虑到只有\(10^5\)种笑话,我们可以开\(10^5\)颗动态开点线段树
如果这个区间的人都听过或都没听过,就继续往下,否则在一颗普通线段树上区间赋值
然后我没调出来,样例第5个输出一直是5
可能需要开\(vector\)(因为直接开那么大的数组,还是\(10^5\)颗,必定MLE)
所以我们考虑用\(set\)来维护
建一个结构体\((L,R,Col)\)表示\([L,R]\)区间都是没听过/听过(0/1)
初始化每一种笑话都是\((1,n,0)\)
对于每一个修改操作\((l,r,y)\)表示\([l,r]\)区间的人听到了\(y\)笑话
我们二分一个\(L\)最大且小于等于\(l\)的区间
代码中的写法是因为重载了构造函数所以可以直接这样写,注意参数一定要默认0,给的参数按顺序
枚举\(L\le r\)的区间,删掉这个区间
注意一定不能把++it
写到for上面—,否则会收获RE的好成绩
这是因为it当时已经被删掉了,所以一定要在删之前++it
反手一个区间赋值,闷声大发财
然后两个区间的交区间直接赋成\(!Col\)(听过了赋\(0\))
如果这个区间也是\(Col==1\)就两边合并一下
否则的话就两边剩余的区间插回去
最后把一整段为\(1\)的区间插回去
吐槽一下,为什么百度搜索前几个大仙的题解里面,线段树写的那么鬼啊
复杂度:\(O(n\log n)\)
#include<bits/stdc++.h>
using namespace std;
template<typename T>
inline void read(T& x)
{
x = 0; char c = getchar();
while (!isdigit(c)) c = getchar();
while (isdigit(c)) x = x * 10 + c - '0', c = getchar();
}
#define maxn 100005
struct node
{
int l, r, col;
node(int l = 0, int r = 0, int col = 0) :l(l), r(r), col(col) {}
bool operator < (const node& p) const { return l < p.l; }
};
set<node> s[maxn];
set<node>::iterator it, it2;
int n, m;
struct SegmentTree2
{
#define ls rt<<1
#define rs rt<<1|1
int val[maxn << 2], flag[maxn << 2], len[maxn << 2];
inline void pushup(int rt) { val[rt] = val[ls] + val[rs]; }
inline void pd(int rt, int f)
{
if (f) val[rt] = len[rt];
else val[rt] = 0;
flag[rt] = f;
}
inline void pushdown(int rt)
{
if (flag[rt] == -1) return;
pd(ls, flag[rt]), pd(rs, flag[rt]);
flag[rt] = -1;
}
void build(int rt, int l, int r)
{
len[rt] = r - l + 1;
flag[rt] = -1;
if (l == r) return;
int mid = (l + r) >> 1;
build(ls, l, mid), build(rs, mid + 1, r);
}
void update(int rt, int l, int r, int fr, int to, int v)
{
if (fr <= l && to >= r) return pd(rt, v);
int mid = (l + r) >> 1;
pushdown(rt);
if (fr <= mid) update(ls, l, mid, fr, to, v);
if (to > mid) update(rs, mid + 1, r, fr, to, v);
pushup(rt);
}
int query(int rt, int l, int r, int fr, int to)
{
if (fr <= l && to >= r) return val[rt];
int mid = (l + r) >> 1, ans = 0;
pushdown(rt);
if (fr <= mid) ans += query(ls, l, mid, fr, to);
if (to > mid) ans += query(rs, mid + 1, r, fr, to);
return ans;
}
#undef ls
#undef rs
}S;
int main()
{
read(n), read(m);
S.build(1, 1, n);
for (int i = 1; i <= 100000; ++i) s[i].insert(node(1, n, 0));
for (int i = 1, op, x, y, z; i <= m; ++i)
{
read(op);
if (op == 1)
{
read(x), read(y), read(z);
x -= z, z = z * 2 + x;
x = max(1, x), z = min(n, z);
it = s[y].upper_bound(x);
if (it != s[y].begin()) --it;
int l = x, r = z;
for (; it != s[y].end() && (*it).l <= z;)
{
int L = (*it).l, R = (*it).r, Col = (*it).col;
S.update(1, 1, n, max(L, x), min(R, z), !Col);
if (Col) l = min(l, L), r = max(r, R);
it2 = it; ++it; s[y].erase(it2);
if (!Col && L < x) s[y].insert(node(L, x - 1, 0));
if (!Col && R > z) s[y].insert(node(z + 1, R, 0));
}
s[y].insert(node(l, r, 1));
}
else
read(x), read(y), printf("%d\n", S.query(1, 1, n, x, y));
}
return 0;
}
There is a negligible beginning in all great action and thought.