本文迁移自洛谷原文。

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

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

  • 当 固定时,所有 到 的路的长度均相同。

  • 当 固定时,所有 到 的路的长度均相同。

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

长这样:

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

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

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

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

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

到了这里,你得到了一个路径总长度约为 的做法。

优化一:

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

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

优化二:

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

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

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

所以我们考虑用第 位维护行, 位维护列,就可以做到约 的总长度了。

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

本文迁移自洛谷原文。

前置知识:中国剩余定理

我们钦定 ,然后这个 根据辗转相减法就可以变为

所以说,当 是 的倍数的时候,要求 是 的倍数。

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

构造一个数 ,和一个数组 ,发现 中是由好多素数构成的。

我们枚举 ,求 ,记为 。如果 是某个 的倍数,那么就可以得到 。

为什么呢?

首先, 中的数两两互质,所以在 gcd 中互不影响。其次, 是 的倍数,那么就意味着 是 的倍数,所以 ,移项就得到了上面那个式子。

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

p.s. 这个 正好处于 和 之间,所以我们选取这些数的积做为模数。

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 的毒瘤题。

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

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

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

  • 这个数列里没有

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

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

  • 这个数列里有

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

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

证明:

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

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

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

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

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

综上,写几个 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);
}

本文迁移自洛谷原文。

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

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

题解:

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

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

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

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

总共有 种选法,包含“某一堆”的有 种选法,所以选择的两堆石子中包括“某一堆”的概率就是 。同理,不包括它的概率就是 。

所以,

线性递推就可以求出 了,最后答案就是 。

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 存储,然后暴力。

时间复杂度:

subtask 2:

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

查询用 即可。 为按位异或, 为按位与。

时间&空间复杂度:

subtask 3:

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

我们设定一个阈值 。

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

大约取在能让大约 个点用 bitset 即可。

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

时间&空间复杂度:

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

本文迁移自洛谷原文。

题目大意:

有 天,第 天你会收到 份食物。食物有保质期,第 天收到的食物只能在第 天和第 天食用。

你还有 个朋友。第 个朋友会在第 至 天拜访你,食量是 份食物。

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

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

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

题解:

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

转移:

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

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

具体可以参考代码

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

本文迁移自洛谷原文。

题目大意:

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

题解:

期望 dp

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

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

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

这个trick在期望题中很常用

考虑转移。

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

卖掉了原有的等级为 的装备,可能性为 ,收益为

  • 拿到了这种装备,但等级不为

那就相当于拿到了又卖了

可能性为 ,收益为

  • 拿到了其它装备

可能性为 ,对其它装备有影响,而对这个装备的收益为

但发现这个方程的转移是 的,无法通过。

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

考虑每一次升级的概率。

从 级升到 的期望次数为 ,所以从 级升到 级的期望次数为

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

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

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%