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);
}
}
}
