本文迁移自洛谷原文

题目有一个很强的提示是“无论怎么走都要能判断出来”,这启示我们可能有很多道路的长度都是相同的。

然后,我们可以这么设计:

  • 当 $i$ 固定时,所有 $a_{i,j}$ 到 $a_{i+1,j}$ 的路的长度均相同。

  • 当 $j$ 固定时,所有 $a_{i,j}$ 到 $a_{i,j+1}$ 的路的长度均相同。

这样,就避免了在不同行中折返走(列同理)的情况。

长这样:

红色是折返走的地方,虽然在不同行但效果一样,如果我们把他们的路径长度赋为相同的权值就可以异或掉了。

通过这个折返走我们可以发现行/列是可以分开考虑的,再由异或想到我们可以用前 $5$ 个二进制位表示行,后 $5$ 个来表示列,所以我们就先对行单独进行分析。

问题就可以被简化成一个一维的图( $1$ 列 $n$ 行),你需要对这些路赋值,需要能判断出走到哪儿了。

我们尝试对 $a_{i,1}$ 赋值为 $i$ (注意,是格子,不是路径长度!$a_{i,1}$ 是假设我们已经把它拍扁成一维了),再让 $a_{i,1}$ 到 $a_{i+1,1}$ 的路径的长度赋值为 $i \oplus (i-1)$ ( $\oplus$ 表示异或)。

假设我们当前在的格子的权值为 $t$,询问给你的路径异或值为 $x$,那么你最后到达的格子的权值就是 $t \oplus x$。由于所有格子的权值都唯一,你就可以做到唯一确定了。

到了这里,你得到了一个路径总长度约为 $1.3\times10^5$ 的做法。

优化一:

你看着这个 $i \oplus (i-1)$ 写成二进制后很不舒服,每条路径的长度都有很多个 $1$。然后你想精简它,虽然题目说了长度必须是正整数不能为 $0$,所以最好就让这个长度在二进制下只有一个 $1$。根据你在CSP2019的经验,你想到了格雷码这个东西,编码中相邻的两个数在二进制下恰好相差一位!

这边改改那边改改,你得到了一个总长度约为 $8\times10^4$ 的做法。很遗憾还是过不去

优化二:

再一次观察每条路径的长度。突然发现,路径长度出现的频率是不同的。比如,$2^0$ 出现的频率是最大的,$2^1$ 次之,到 $2^4$ 都是大幅减小,但到了 $2^5$ 又变得和 $2^0$ 一样多了。

探究出现的原因,发现是行&列位数分配不当的原因,原来的方案是对于所有的列的路径长度就要直接乘上 $2^5$。

但这个 $2^4$ 啊,长度很小,但频率却非常小,感觉上就没有被充分利用。

所以我们考虑用第 $0,2,4,6,8$ 位维护行,$1,3,5,7,9$ 位维护列,就可以做到约 $4.7\times 10^4$ 的总长度了。

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
int gc[114514];
int rgc[114514];
inline int GET(int x){
return gc[x];
}
inline void prep(){//计算格雷码
gc[0]=0;
for(int i=0;i<6;++i){
int csz=1<<i;
for(int j=0;j<csz;++j){
gc[csz*2-j-1]=csz+gc[j];
}
}
for(int i=0;i<=32;++i)rgc[gc[i]]=i;
}
inline int HANG(int x){//行列分离
int rt=0;
for(int i=0,j=0;i<5;j+=2,++i){
if(x&(1<<i)){
rt+=(1<<j);
}
}
return rt;
}
inline int LIE(int x){
int rt=0;
for(int i=0,j=1;i<5;j+=2,++i){
if(x&(1<<i)){
rt+=(1<<j);
}
}
return rt;
}
inline int RHANG(int x){
int rt=0;
for(int i=0,j=0;i<5;j+=2,++i){
if(x&(1<<j)){
rt+=(1<<i);
}
}
return rt;
}
inline int RLIE(int x){
int rt=0;
for(int i=0,j=1;i<5;j+=2,++i){
if(x&(1<<j)){
rt+=(1<<i);
}
}
return rt;
}
int k;
inline void solve(){
int n;cin>>n>>k;
fflush(stdin);
for(int i=1;i<=n;++i){
for(int j=1;j<n;++j){
cout<<LIE(GET(j)^GET(j-1))<<' ';//0 2 4 6 8
fflush(stdout);
}
cout<<endl;fflush(stdout);
}
for(int i=1;i<n;++i){
for(int j=1;j<=n;++j){
cout<<HANG(GET(i)^GET(i-1))<<' ';//1 3 5 7 9
fflush(stdout);
}
cout<<endl;fflush(stdout);
}
fflush(stdin);
int curh=0,curl=0;
for(;k--;){
int x;cin>>x;fflush(stdin);
int l=RLIE(x),h=RHANG(x);
curh^=h,curl^=l;
cout<<rgc[curh]+1<<' '<<rgc[curl]+1<<endl;
fflush(stdout);
}
}
int main(){
prep();
int T=1;
//cin>>T;
for(;T--;)solve();
return (0-0);
}

本文迁移自洛谷原文

前置知识:中国剩余定理

我们钦定 $a<b$,然后这个 $gcd(x+a,x+b)$ 根据辗转相减法就可以变为 $gcd(b-a,x+a)$

所以说,当 $gcd(x+a,x+b)$ 是 $b-a$ 的倍数的时候,要求 $x+a$ 是 $b-a$ 的倍数。

我们考虑如何充分利用这个性质。

构造一个数 $a=2\times 3\times 7 \times 11 \times 13 \times 17 \times 19 \times 23 \times 29$,和一个数组 $b={2,3,7,11,13,17,19,23,29}$,发现 $a$ 中是由好多素数构成的。

我们枚举 $i=0\dots28$,求 $gcd(x+i,x+a+i)$,记为 $r_i$。如果 $r_i$ 是某个 $b_j$ 的倍数,那么就可以得到 $x\mod b_j= b_j-i \mod b_j$。

为什么呢?

首先,$b$ 中的数两两互质,所以在 gcd 中互不影响。其次,$gcd(x+i,x+a+i)$ 是 $b_j$ 的倍数,那么就意味着 $x+i$ 是 $b_j$ 的倍数,所以 $x+i \mod b_j = 0$,移项就得到了上面那个式子。

最后发现这就是中国剩余定理的板子,直接套即可。

p.s. 这个 $a$ 正好处于 $10^9$ 和 $2\times 10^9$ 之间,所以我们选取这些数的积做为模数。

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define reg register
int rem[29];
int pri[]={2,3,7,11,13,17,19,23,29};
ll n,a[16],m[16],Mi[16],mul=1,X;
void exgcd(ll a,ll b,ll &x,ll &y){
if(b==0){x=1;y=0;return ;}
exgcd(b,a%b,x,y);
int z=x;x=y,y=z-y*(a/b);
}
inline void solve(){
for(int i=1;i<=29;++i){
cout<<"? "<<i<<' '<<1293938646+i<<endl;
fflush(stdout);
int x;cin>>x;fflush(stdin);
for(int j=0;j<9;++j)if(x%pri[j]==0)rem[j]=pri[j]-(i%pri[j]);
}
n=9;mul=1;memset(m,0,sizeof(m));X=0;memset(Mi,0,sizeof(Mi));
for(int t=1;t<=n;++t){
m[t]=pri[t-1];
mul*=m[t];
a[t]=rem[t-1];
}
for(int t=1;t<=n;++t){
Mi[t]=mul/m[t];
ll x=0,y=0;
exgcd(Mi[t],m[t],x,y);
X+=a[t]*Mi[t]*(x<0?x+m[t]:x);
}
cout<<"! "<<X%mul<<'\n';
}
int main(){
// ios_base::sync_with_stdio(false);
// cin.tie(0),cout.tie(0);
int T=1;
cin>>T;
for(;T--;)solve();
return (0-0);
}

本文迁移自洛谷原文

送 300iq 下 2400 的毒瘤题。

这题带有一定的诈骗性质。

首先,如果这个数列中即含有 $0$ 又含有 $1$,那么它肯定就是不合法的,因为模数要 $\geq 2$,所以 $1$ 永远是 $1$,$0$ 永远是 $0$。

然后我们对这个数列有没有 $1$ 进行分类讨论。

  • 这个数列里没有 $1$

诈骗。我们对这个数组按照从大到小排序,每次选择最大的那个数做为模数,让所有数对它取模即可。

因为他是最大的数,所以所有其他数肯定小于等于他。小于它的不变,等于它的和他一起变为 $0$。最终所有数都变为 $0$。

  • 这个数列里有 $1$

我们已经知道 $1$ 是动不了的,所以我们想让所有其他数都变为 $1$。

有个结论,就是当这个序列中存在两个连续的数时,它不合法;反之,他一定合法。

证明:

1.不存在两个连续的数时,这个序列合法

这个比较好证,还是从大到小排,对第 $i$ 大的数 $a_i$,让所有数都对 $a_i-1$ 取模即可。(因为不存在 $a_i-1$,所以不会有数变为 $0$)

2.存在两个连续的数时,这个序列不合法

设存在的两个连续的数为 $x,x+1$,那么无论取什么数做为模数,他们都不可能相等。

(他们之差为 $1$,只有当模数为 $1$ 的时候这个 $1$ 才能被消去,故不可能)

综上,写几个 if 和 for 就完事了

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
using namespace std;
const int mxn=2e5+5;
int n,a[mxn];
inline void solve(){
cin>>n;
int hv0=0,hv1=0;
for(int i=1;i<=n;++i){
cin>>a[i];
if(a[i]==0)hv0=1;
if(a[i]==1)hv1=1;
}
sort(a+1,a+n+1);
if(hv0 and hv1){
cout<<"NO\n";
return;
}
if(!hv1){
cout<<"YES\n";
return;
}
for(int i=1;i<n;++i)if(a[i+1]==a[i]+1){
cout<<"NO\n";
return;
}
cout<<"YES\n";
}
int main(){
ios_base::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
int T=1;
cin>>T;
for(;T--;)solve();
return (0-0);
}

本文迁移自洛谷原文

说句题外话,这题刚搬过来时精度要求是 $10^{-10}$ 级别的,人直接崩溃

然后顺便拿到了在洛谷上的一血

题解:

显然如果不考虑价值只考虑算进答案的次数的话,所有堆都是相同的。

所以我们可以计算每一堆石子期望被算进答案的次数。(如果合并的一堆产生了贡献,组成它的所有堆都相当于被算进了答案一次)

设 $f(n)$ 表示还剩下 $n$ 堆石子的时候,每堆石子以后被算进答案的次数的期望。

由于每堆石子是等价的,所以他们被选中的概率相同。

总共有 $\frac{n\times(n-1)}{2}$ 种选法,包含“某一堆”的有 $n-1$ 种选法,所以选择的两堆石子中包括“某一堆”的概率就是 $\frac{2}{n}$。同理,不包括它的概率就是 $\frac{n-2}{n}$。

所以,$f(n)=(1+f(n-1))\times \frac{2}{n} + f(n-1)\times(\frac{n-2}{n})=\frac{2}{n}+f(n-1)$

线性递推就可以求出 $f$ 了,最后答案就是 $f(n)\times\sum{a_i}$。

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define ld long double
inline void solve(){
ll n;ld x,sum=0,ml=0;
cin>>n;
for(int i=1;i<=n;++i)cin>>x,sum+=x;
for(int i=2;i<=n;++i)ml+=(ld)(2)/(ld)(i);
cout<<fixed<<setprecision(9)<<sum*ml<<'\n';
}
int main(){
ios_base::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
int T=1;
//cin>>T;
for(;T--;)solve();
return (0-0);
}

本文迁移自洛谷原文

2020年的J组模拟,2022年终于进主题库了

知识点:根号分治

subtask 1:

利用 map 存储,然后暴力。

时间复杂度: $O(n^2\log n)$

subtask 2:

对于每一个节点都开一个 bitset 存储邻接点。(设为 $b_i$ )

查询用 $b_u \ xor \ (b_u \ and \ b_v)$ 即可。$xor$ 为按位异或,$and$ 为按位与。

时间&空间复杂度: $O(\frac{n^2}{\omega})$

subtask 3:

考虑将 subtask 1 和 2 结合起来。

我们设定一个阈值 $limit$。

对于所有度数大于 $limit$ 的节点,我们用 bitset。反之,我们用 map。

$limit$ 大约取在能让大约 $\sqrt{n}$ 个点用 bitset 即可。

查询的时候要分类讨论。(具体见代码)

时间&空间复杂度:$O(n\times\sqrt{\frac{n\log n}{\omega}})$

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include<bits/stdc++.h>
#define ll long long
#define reg register
#define mp make_pair
#define ri register int
#define ld long double
using namespace std;
const int mxn=2e5+5;
vector<int>g[mxn];
int n,m,q;
int lim;
vector<int>heavy;
int id[mxn];
vector<bitset<mxn> >T;
map<int,int>have[mxn];
int deg[mxn];
inline void solve(){
scanf("%d%d%d",&n,&m,&q);
//cerr<<lim<<'\n';
for(ri i=1,u,v;i<=m;++i){
scanf("%d%d",&u,&v);
g[u].push_back(v);
g[v].push_back(u);
++deg[u];
++deg[v];
}
sort(deg+1,deg+n+1);reverse(deg+1,deg+n+1);
lim=deg[min(n,(int)(sqrt(n)*6))];//根号处分治
memset(id,-1,sizeof(id));
for(ri i=1;i<=n;++i){
if(g[i].size()>lim){
heavy.push_back(i);
id[i]=heavy.size()-1;bitset<mxn>newb;
newb&=0;
for(ri j=0;j<g[i].size();++j)newb[g[i][j]]=1;
T.push_back(newb);
}else{
for(ri j=0;j<g[i].size();++j)have[i][g[i][j]]=1;
}
}
for(ri i=1;i<=q;++i){
ri u,v;scanf("%d%d",&u,&v);
if(~id[u] and ~id[v]){ //对两个点是否是重点分类讨论
bitset<mxn>tmp=T[id[u]]^(T[id[u]]&T[id[v]]);
tmp[u]=0;tmp[v]=0;
printf("%d\n",tmp.count());
}else if(~id[u]){
ri ans=0;
for(ri j=0;j<g[v].size();++j)if(g[v][j]==u or g[v][j]==v or T[id[u]][g[v][j]])++ans;
printf("%d\n",T[id[u]].count()-ans);
}else if(~id[v]){
ri ans=0;
for(ri j=0;j<g[u].size();++j)if(g[u][j]!=u and g[u][j]!=v and !T[id[v]][g[u][j]])++ans;
printf("%d\n",ans);
}else{
ri ans=0;
for(ri j=0;j<g[u].size();++j)if(g[u][j]!=u and g[u][j]!=v and !have[v][g[u][j]])++ans;
printf("%d\n",ans);
}
}
}
int main(){
solve();
return 0;
}

本文迁移自洛谷原文

题目大意:

有 $n$ 天,第 $i$ 天你会收到 $a_i$ 份食物。食物有保质期,第 $i$ 天收到的食物只能在第 $i$ 天和第 $i+1$ 天食用。

你还有 $m$ 个朋友。第 $i$ 个朋友会在第 $l_i$ 至 $r_i$ 天拜访你,食量是 $c_i$ 份食物。

你每天可以选择一些朋友供应食物(必须供给正好 $c_i$ 份食物,一个朋友一天只能供给一次),每贡献一人次你的人气值就会 $+1$。

同时,你每天必须吃 $v$ 份食物。

问你 $n$ 天后你的人气值最大是多少。

题解:

令 $dp_{i,j}$ 表示考虑到第 $i$ 天,昨天剩下了 $j$ 份食物时你最大的人气值。

转移:

很显然,我们每一天肯定会先吃昨天的剩饭,不够了再吃今天的饭。

然后选择朋友的话,我们可以贪心的选:按照朋友的饭量从小到大排序,选最小的前 $k$ 个。

具体可以参考代码

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
const int mxn=444;
int n,v,m;
int a[mxn];
vector<pair<int,int> >c[mxn];
int dp[mxn][mxn*2],pre[mxn][mxn*2],preg[mxn][mxn*2]; //pre[][]记录剩饭由哪儿转移过来,preg记录了当天请了饭量前多少小的朋友吃饭
inline void solve(){
cin>>n>>v;
for(int i=1;i<=n;++i)cin>>a[i];
cin>>m;
for(int i=1;i<=m;++i){
int l,r,t;cin>>l>>r>>t;
for(int j=l;j<=r;++j)c[j].push_back(mp(t,i));
}
for(int i=1;i<=n;++i)sort(c[i].begin(),c[i].end());
for(int i=0;i<mxn;++i)for(int j=0;j<mxn*2;++j)dp[i][j]=-1145141;
dp[1][0]=0;
for(int i=1;i<=n;++i){
for(int j=0;j<=800;++j){//dp[i][j]表示考虑到第i天,昨天剩下了j的食物所能得到的最大值
if(dp[i][j]<0)continue;
int yes=j,tod=a[i]; //分别代表昨天的剩饭和今天新增的饭
if(yes>=v)yes-=v;
else tod-=(v-yes),yes=0; //先处理今天自己吃的饭
int x=tod+yes;
if(dp[i][j]>dp[i+1][tod]){
dp[i+1][tod]=dp[i][j];
pre[i+1][tod]=j;
preg[i+1][tod]=0;
}
int sum=0;
for(int k=0;k<c[i].size();++k){
sum+=c[i][k].first;
if(sum>x)break;
int rem=min(tod,x-sum); //还是先吃剩饭,然后处理昨天不能保留到明天的饭
if(dp[i][j]+k+1>dp[i+1][rem]){
dp[i+1][rem]=dp[i][j]+k+1;
pre[i+1][rem]=j;
preg[i+1][rem]=k+1;
}
}
}
}
int pos=0;
for(int i=1;i<=800;++i)if(dp[n+1][i]>dp[n+1][pos])pos=i;
cout<<dp[n+1][pos]<<'\n';
vector<int>bk;
bk.clear();
int lst=pos;
for(int i=n+1;i>1;--i){
bk.push_back(preg[i][lst]);
lst=pre[i][lst];
}
for(int i=bk.size()-1;~i;--i){
cout<<bk[i]<<' ';
for(int j=0;j<bk[i];++j)cout<<c[bk.size()-i][j].second<<' ';
cout<<'\n';
}
}
int main(){
freopen("input.txt","r",stdin);
freopen("output.txt","w",stdout);
ios_base::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
int T=1;
//cin>>T;
for(;T--;)solve();
return 0;
}

本文迁移自洛谷原文

题目大意:

一款游戏中有 $k$ 种装备,你一开始每种装备各有 $1$ 个,且每种装备的初始等级均为 $1$。游戏中可以靠打怪来获取新装备,总共有 $n$ 只怪兽,每打赢 $1$ 只怪兽后会随机获得一种装备 $a \in [1,k]$。假设原有的 $a$ 装备的等级为 $t$,那么新获得的装备的等级为 $[1,t+1]$,且你会将新获得的装备和原来的装备中等级较高的装备留下,等级较低的装备卖出,卖出可获得的金币为该装备的等级。 问打完这 $n$ 只怪兽后,获得的金币的期望。

$1 \le n \le 10^5, 1 \le k \le 100$

题解:

期望 dp

观察到这 $k$ 种装备都是“一样”的,所以我们可以随便选择一种装备来考虑它在打这 $n$ 个怪兽时是如何变化的,然后对答案乘上 $k$ 就可以了。

令 $dp_{i,j}$ 表示考虑到 还剩 $i$ 个怪兽没打,且此时装备的等级为 $j$ 时的期望答案。

为什么是 还剩 $i$ 个怪兽,而不是 打了 $i$ 个怪兽呢?因为如果我们如果设计是打了 $i$ 个怪兽的话,每次往后转移的时候还要考虑到这种情况出现的概率,不好处理。而如果从后往前转移的话,就避免了复杂的计算。

这个trick在期望题中很常用

考虑转移。

  • 正好拿到了这种装备,而且等级为 $j+1$

卖掉了原有的等级为 $j$ 的装备,可能性为 $\frac{1}{k\times(j+1)}$,收益为 $j$

  • 拿到了这种装备,但等级不为 $j+1$

那就相当于拿到了又卖了

可能性为 $\frac{j}{(j+1)\times k}$,收益为 $\frac{\sum\limits_{f=1}^{j}f}{j}=\frac{j\times(j+1)}{2\times j}=\frac{j+1}{2}$

  • 拿到了其它装备

可能性为 $\frac{k-1}{k}$,对其它装备有影响,而对这个装备的收益为 $0$

但发现这个方程的转移是 $O(n^2)$ 的,无法通过。

你想了想,发现这题要求输出的是小数,就打算在这上面动心思。

考虑每一次升级的概率。

从 $j$ 级升到 $j+1$ 的期望次数为 $k\times(j+1)$,所以从 $1$ 级升到 $j+1$ 级的期望次数为 $\sum\limits_{f=1}^{j}k\times(j+1)=k\times\sum\limits_{f=1}^{j}f=\frac{k\times j \times (j+1)}{2}$

发现到了后面就很大可能不会再升级了。

所以,我们可以假定一个最大等级 $l=1000$,当达到最大等级后就不再升级,这样也是在误差范围内的。

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using namespace std;
int n;
#define ld long double
ld dp[2][1005],k; //滚动了下,防炸空间
inline void solve(){
cin>>n>>k;
int cur=0,pre=1;
for(int i=1;i<=n;++i){
for(int j=1;j<=1000;++j){
dp[cur][j]=0;
dp[cur][j]+=(dp[pre][j+1]+j)/(j+1)/k;
dp[cur][j]+=(dp[pre][j]+(j+1.0)/2.0)*1.0*j/k/(1.0*(j+1));
dp[cur][j]+=(dp[pre][j])*(k-1)/k;
}
swap(cur,pre);
}
cout<<fixed<<setprecision(9)<<dp[pre][1]*k<<'\n';
}
int main(){
ios_base::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
int T=1;
//cin>>T;
for(;T--;)solve();
return 0;
}
0%