本文迁移自洛谷原文

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

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

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

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

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

长这样:

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

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

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

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

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

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

优化一:

你看着这个 写成二进制后很不舒服,每条路径的长度都有很多个 。然后你想精简它,虽然题目说了长度必须是正整数不能为 ,所以最好就让这个长度在二进制下只有一个 根据你在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%