The 2021 Shanghai Collegiate Programming Contest 部分题解
The 2021 Shanghai Collegiate Programming Contest 部分题解
这场比赛是我自己VP打的,题不难但是有很多不必要的WA,需要以此为戒
A.
题意
给定两个三维向量\((x_1,y_1,z_1)\) ,\((x_2,y_2,z_2)\) 求和这两个向量都垂直的一个向量
分析
我们有熟知的结论,三维空间中,两个向量的叉积就是这两个向量确定平面的法向量
本题由于有范围限制,因此也可以枚举每一个向量,判断是否同时和两向量的点乘为0
代码
int main(){
int x = rd();
int y = rd();
int z = rd();
int xx = rd();
int yy = rd();
int zz = rd();
for(int i = -200;i <= 200;i++)
for(int j = -200;j <= 200;j++)
for(int k = -200;k <= 200;k++)
if(i * x + j * y + k * z == 0 && i * xx + j * yy + k * zz == 0) {
printf("%d %d %d\n",i,j,k);
return 0;
}
}
B.
题意
给出\(n\)个三元组,取其中\(a\)个1号,\(b\)个2号,\(c\)个3号,且保证\(a +b + c = n\) ,问能够取到的最大值
分析
题目显然要求我们\(O(n^2polylog)\)的做法,暴力\(DP\)的复杂度是\(O(n^3)\)的
考虑二元组的情况,这个时候显然贪心从二元组差值大的开始取是最优的
于是可以\(dp[i][j]\)表示前\(i\)个物品,选择了\(j\)个三号物品,这样每次不选三号物品的时候只需要按照之前的贪心策略选
代码
实现的时候需要注意,应当给\(DP\)数组初始化,否则从\(dp[i-1][j]\)的时候会出问题
const int maxn = 5e3 + 5;
struct S{
int a,b,c;
S(){}
S(int _a,int _b,int _c){
a = _a;
b = _b;
c = _c;
}
friend bool operator < (const S &x,const S &y){
if(x.b - x.a == y.b - y.a) return x.c > y.c;
return x.b - x.a > y.b - y.a;
}
};
ll dp[maxn][maxn];
int main(){
int n = rd();
int a = rd();
int b = rd();
int c = rd();
vector<S> v(n + 1);
for(int i = 1;i <= n;i++){
v[i].a = rd();
v[i].b = rd();
v[i].c = rd();
}
sort(v.begin() + 1,v.end());
for(int i = 0;i <= n;i++)
for(int j = i + 1;j <= c;j++)
dp[i][j] = -1e18;
for(int i = 1;i <= n;i++){
for(int j = 0;j <= min(c,i);j++){
if(j) dp[i][j] = max(dp[i][j],dp[i - 1][j - 1] + v[i].c);
if(i - j <= b) dp[i][j] = max(dp[i][j],dp[i - 1][j] + v[i].b);
else dp[i][j] = max(dp[i][j],dp[i - 1][j] + v[i].a);
}
}
printf("%lld",dp[n][c]);
}
C.
代码
int main(){
int n = rd();
int m = rd();
int tot = 0;
vector<pii> v(n);
VI ans(n + 1);
for(int i = 0;i < n;i++){
v[i].se = rd();
v[i].fi = rd();
tot += v[i].fi;
}
for(int i = 0;i < n;i++){
if(v[i].se != m) {
if(v[i].fi * n >= tot) v[i].fi -= 2,v[i].fi = max(v[i].fi,0ll);
}
else {
if(v[i].fi < 60) v[i].fi = 60;
}
ans[v[i].se] = v[i].fi;
}
for(int i = 1;i <= n;i++)
printf("%d ",ans[i]);
}
D.
给定一个可重集,要求构造\(2 \times n\) 的序列,这个序列需要满足每一行从左到右不递减,第一行比第二行不递减,数字带标号 求可能的方案数
分析
此题有点像 ACWING271. 杨老师的照相排列,做法应该可以很快确定DP即可。难点在于难以找到有效的DP转移方法。
考虑类似插入型DP那样,\(DP\)的时候按照大小顺序插入就变得很方便。因为两行之间有偏序关系,假设从大到小插入数,如果能够钦定第一行比第二行的人多,那么转移的时候枚举当前人插在第一行还是第二行,就能保证要求的条件满足。
因为相同大小的数可以随意放,因此每次枚举每一种数就行,\(dp[i][j]\)表示前\(i\)个人,第一行比第二行多\(j\)个人时的方案。用\(t\)表示当前数的第一行比第二行多的个数,\(c[i]\)表示\(i\)的个数,不难得到转移方程
代码
int main(){
int n = rd();
factPrework(n);
for(int i = 1;i <= n;i++){
a[i] = rd();
c[a[i]]++;
}
dp1[0] = 1;
for(int i = 1;i <= n;i++){
if(!c[i]) continue;
for(int j = 0;j <= n;j++) dp2[j] = dp1[j],dp1[j] = 0;
for(int j = 0;j <= i;j++){
for(int k = 0;k <= c[i];k++){
int t = k - (c[i] - k);
int x = (i - (j + t)) / 2;
if(j + t >= 0 && j + t + x <= n) {
add(dp1[j + t],mul(mul(dp2[j],mul(fac[k],fac[c[i] - k])),C(c[i],k)));
}
}
}
}
printf("%d",dp1[0]);
}
E
只要懂基本的期望知识即可
代码
char ch[3];
int main(){
int n = rd();
int k = rd();
double ans = 0;
for(int i = 1;i <= n;i++){
scanf("%s",ch);
double p;
scanf("%lf",&p);
if(ch[0] == 'D') {
ans += p * 16;
}
else if(ch[0] == 'C') {
ans += p * 24;
}
else if(ch[0] == 'B'){
ans += p * 54;
}
else if(ch[0] == 'A'){
ans += p * 80;
}
else {
ans += p * 10000;
}
}
ans *= k;
ans -= k * 23;
printf("%.10f",ans);
}
G
题意
给出\(n\)个数,\(P = \prod a_i\) 求\(ans_i = \frac{P}{a_i} \ mod \ 998244353\)
分析
幸好这道题没在正式比赛出,否则完了。没有注意到\(998244353\)在模\(998244353\)下没有逆,直接用逆元去做WA了两发,这样的话得特判掉998244353这种情况。
事实上可以直接维护前缀积和后缀积
代码
SB讨论
int main(){
int n = rd();
VI v(n + 1);
int ans = 1;
int res = 1;
int cnt = 0;
for(int i = 1;i <= n;i++){
v[i] =rd();
ans = mul(ans,v[i]);
if(v[i] != MOD) res = mul(res,v[i]);
else cnt++;
}
if(cnt > 1) {
for(int i = 1;i <= n;i++)
printf("0 ");
return 0;
}
else
for(int i = 1;i <= n;i++){
if(v[i] != MOD)
printf("%d ",mul(ans,ksm(v[i])));
else printf("%d ",res);
}
}
J
题意
两人轮流取卡片,获得的价值是所有物品的价值的和的绝对值,先后手都希望两人的最终价值比对方的越大越好
分析
其实比较感性得也可以理解直接取最大的即可。
严格的讲 假设Alice获得价值为\(|A|\),Bob获得的\(|B|\),本质都想使自己价值尽可能大。
由于所获价值都带有绝对值,因此对所有数取反并不会影响答案,我们不失一般性地设\(S = \sum a_i \geq 0\)
那么
可知ans具有单调性,所以只要每次都取最大的数即可
代码
int a[5005];
int main(){
int n = rd();
ll tot = 0;
for(int i = 1;i <= n;i++)
a[i] = rd();
sort(a + 1,a + n + 1);
reverse(a + 1,a + n + 1);
ll ans = 0;
for(int i = 1;i <= n;i += 2)
ans += a[i];
ans = abs(ans);
ll res = 0;
for(int i = 2;i <= n;i += 2)
res += a[i];
res = abs(res);
ans = ans - res;
reverse(a + 1,a + n + 1);
ll ans2 = 0;
for(int i = 1;i <= n;i += 2)
ans2 += a[i];
ans2 = abs(ans2);
res = 0;
for(int i = 2;i <= n;i += 2)
res += a[i];
res = abs(res);
ans = max(ans,ans2 - res);
printf("%lld",ans);
}
K
题意
给出\(n\)个字符串,两人轮流操作,不能操作者输
每次有以下两种选择:1.选择一个非空字符串,取走任意一个字符。2.选择一个非空字符串,取走任意两个不同字符
分析
注意到对一个字符串来说,选择的位置没有限制,即选择只和第二个操作带来的:不同的字符个数有关。
可以打表得到\(\sum_{i=1}^{40} P(i) = 215308\) 状态数不多,因此只需要暴力求SG函数,这里为了减少常数,使用了对集合的哈希
代码
const ull base = 131;
const int maxn = 45;
unordered_map<ull,int> vis;
ull get_hash(VI &cur){
ull res = 0,fac = 1;
for(auto &it: cur){
res += fac * it;
fac = fac * base;
}
return res;
}
int dfs(VI cur){
sort(cur.rbegin(),cur.rend());
ull Hash = get_hash(cur);
if(vis.count(Hash)) return vis[Hash];
if(!Hash) return 0;
set<int> st;
for(int i = 0;i < (int)cur.size();i++){
if(cur[i]) {
cur[i]--;
st.insert(dfs(cur));
cur[i]++;
}
else break;
}
for(int i = 0;i < (int)cur.size();i++){
if(!cur[i]) break;
for(int j = i + 1;j < (int)cur.size();j++){
if(cur[j]) {
cur[i]--;
cur[j]--;
st.insert(dfs(cur));
cur[i]++;
cur[j]++;
}
else break;
}
}
int Mex = 0;
for(auto &it:st){
if(it != Mex) break;
Mex++;
}
return vis[Hash] = Mex;
}
char s[45];
int main(){
int T = rd();
while(T--){
int n = rd();
int ans = 0;
for(int i = 0;i < n;i++){
scanf("%s",s);
int len = strlen(s);
VI cnt(26);
for(int j = 0;j < len;j++)
cnt[s[j] - 'a']++;
ans ^= dfs(cnt);
}
if(ans) puts("Alice");
else puts("Bob");
}
}
分拆数可以用\(O(n^2)\)的递推求得,也可以用\(O(n\sqrt{n})\)五边形数定理求得
int get(int x){
int ans = 0;
for(int i = 1;i * i <= x;i++){
if(x % i) continue;
ans += i;
if(i * i == x) break;
ans += x / i;
}
return ans;
}
int dp[45];
int main(){
int ans = 0;
dp[0] = 1;
for(int i = 1;i <= 40;i++){
for(int j = 0;j <= i - 1;j++)
dp[i] += get(i - j) * dp[j];
dp[i] /= i;
}
cout << dp[19]
}
int w[maxn];
int f[maxn];
int main(){
int k = 1;
for(int i = 1;w[k - 1] <= maxn - 5;i++){
w[k++] = (3 * i * i - i) / 2;
w[k++] = (3 * i * i + i) / 2;
}
f[0] = 1;
for(int i = 1;i <= maxn - 5;i++){
for(int j = 1;w[j] <= i;j++){
if(((j - 1) >> 1) & 1) add(f[i],MOD - f[i - w[j]]);
else add(f[i],f[i - w[j]]);
}
}
int T = rd();
while(T--){
int n = rd();
printf("%d\n",f[n]);
}
}
I
题意
要求维护三个序列。支持以下4种操作
1.查询\(x\)个序列区间和
2.第\(x\)个序列区间加\(v\)
3.第\(x\)个序列和第\(y\)个序列区间对应位置交换
4.第\(x\)个序列区间加上第\(y\)个序列对应位置的值
分析
三中操作可以对应线性代数中的初等变换,这些初等变换可以看做一个列向量左乘一个初等矩阵。
如
显然这样的$3 \times 3 $的矩阵就可以做操作34了,操作2,则需要额外维护一个信息。
因此线段树上每个节点维护一个\(4 \times 1\)的列向量即可,每次区间乘一个\(4 \times 4\)的初等矩阵
代码
struct mat{
int a[4][4];
mat(){memset(a,0,sizeof a);}
mat operator * (const mat &c) const {
mat res;
for(int i = 0;i < 4;i++)
for(int j = 0;j < 4;j++)
for(int k = 0;k < 4;k++)
add(res.a[i][j],(ll)a[i][k] * c.a[k][j] % MOD);
return res;
}
bool operator != (const mat &c) const{
for(int i = 0;i < 4;i++)
for(int j = 0;j < 4;j++)
if(a[i][j] != c.a[i][j]) return true;
return false;
}
}I;
inline void mul(int *arr,mat &c){
int tmp[4] = {0};
for(int i = 0;i < 4;i++)
for(int j = 0;j < 4;j++)
add(tmp[i],(ll)c.a[i][j] * arr[j] % MOD);
for(int i = 0;i < 4;i++)
arr[i] = tmp[i];
}
const int maxn = 3e5 + 5;
int sum[maxn << 2][4];
struct SegmentTree{
int n;
vector<mat> tag;
SegmentTree(int n):n(n),tag(((n + 1) << 2)) {}
inline void push_up(int i){
for(int j = 0;j < 4;j++)
sum[i][j] = (sum[i << 1][j] + sum[i << 1|1][j]) % MOD;
}
void build(int i,int l,int r){
tag[i] = I;
if(l == r) {
sum[i][0] = 1;
return;
}
int mid = l + r >> 1;
build(i << 1,l,mid);
build(i << 1|1,mid + 1,r);
sum[i][0] = sum[i << 1][0] + sum[i << 1|1][0];
if(sum[i][0] >= MOD) sum[i][0] -= MOD;
}
inline void update(int i,mat &v){
tag[i] = v * tag[i];
mul(sum[i],v);
}
inline void push(int i){
if(tag[i] != I) {
update(i << 1,tag[i]);
update(i << 1|1,tag[i]);
tag[i] = I;
}
}
void update(int i,int l,int r,int L,int R,mat &v){
if(l > R || r < L) return;
if(l >= L && r <= R) return update(i,v);
int mid = l + r >> 1;
push(i);
update(i << 1,l,mid,L,R,v);
update(i << 1|1,mid + 1,r,L,R,v);
push_up(i);
}
int query(int i,int l,int r,int L,int R,int x){
if(l > R || r < L) return 0;
if(l >= L && r <= R) return sum[i][x];
int mid = l + r >> 1;
push(i);
return (query(i << 1,l,mid,L,R,x) + query(i << 1|1,mid + 1,r,L,R,x)) % MOD;
}
};
int main(){
I.a[0][0] = I.a[1][1] = I.a[2][2] = I.a[3][3] = 1;
int n = rd();
int q = rd();
SegmentTree seg(n);
seg.build(1,1,n);
while(q--){
int op = rd();
if(op == 0) {
int x = rd();
int l = rd();
int r = rd();
printf("%d\n",seg.query(1,1,n,l,r,x));
}
else if(op == 1) {
int x = rd();
int l = rd();
int r = rd();
int y = rd();
mat tmp = I;
tmp.a[x][0] = y;
seg.update(1,1,n,l,r,tmp);
}
else if(op == 2){
int x = rd();
int y = rd();
int l = rd();
int r = rd();
mat tmp = I;
if(x != y) tmp.a[x][y] = tmp.a[y][x] = 1,tmp.a[x][x] = tmp.a[y][y] = 0;
seg.update(1,1,n,l,r,tmp);
}
else{
int x = rd();
int y = rd();
int l = rd();
int r = rd();
mat tmp = I;
tmp.a[y][x]++;
seg.update(1,1,n,l,r,tmp);
}
}
}