2025.1.12-1.19 日志
1.12
CF49E
Difficulty:2300 Tag:区间DP
#include<bits/stdc++.h>
using namespace std;
const int N=60;
string s1,s2;
bool dp1[N][N][30],dp2[N][N][30];
int dp[N][N];
map<int,vector<pair<int,int> > > mp;
int n,len1,len2;
int main(){
ios::sync_with_stdio(false);
cin.tie(NULL),cout.tie(NULL);
cin>>s1>>s2;
cin>>n;
len1=s1.size(),len2=s2.size();
for(int i=1;i<=n;++i){
string str;cin>>str;
mp[str[0]-'a'].push_back(make_pair(str[3]-'a',str[4]-'a'));
}
// for(int i=0;i<mp['c'-'a'].size();++i){
// cout<<mp['c'-'a'][i].first<<' '<<mp['c'-'a'][i].second<<'\n';
// }
//dp1
for(int i=1;i<=len1;++i){
dp1[i][i][s1[i-1]-'a']=1;
}
for(int l=2;l<=len1;++l){
for(int i=1;i<=len1;++i){
int j=i+l-1;
if(j>len1){
break;
}
for(int k=i;k<j;++k){
for(int ch=0;ch<26;++ch){
for(int id=0;id<mp[ch].size();++id){
dp1[i][j][ch]|=dp1[i][k][mp[ch][id].first]&&dp1[k+1][j][mp[ch][id].second];
}
}
}
}
}
//dp2
for(int i=1;i<=len2;++i){
dp2[i][i][s2[i-1]-'a']=1;
}
for(int l=2;l<=len2;++l){
for(int i=1;i<=len2;++i){
int j=i+l-1;
if(j>len2){
break;
}
for(int k=i;k<j;++k){
for(int ch=0;ch<26;++ch){
for(int id=0;id<mp[ch].size();++id){
dp2[i][j][ch]|=dp2[i][k][mp[ch][id].first]&&dp2[k+1][j][mp[ch][id].second];
}
}
}
}
}
//calc ans
memset(dp,0x3f,sizeof(dp));
dp[0][0]=0;
for(int i=1;i<=len1;++i){
for(int j=1;j<=len2;++j){
for(int k=1;k<=i;++k){
for(int p=1;p<=j;++p){
for(int ch=0;ch<26;++ch){
if(dp1[k][i][ch]&&dp2[p][j][ch]){
dp[i][j]=min(dp[i][j],dp[k-1][p-1]+1);
}
}
}
}
}
}
if(dp[len1][len2]<1e9){
cout<<dp[len1][len2]<<'\n';
}
else{
cout<<"-1\n";
}
return 0;
}
一个区间DP的好题,总结以下关键点:
- 区间DP的一般写法:枚举区间长度,枚举两个端点,枚举其它信息,转移。
- 在计算对于两个数组的最优解时,不妨考虑从前缀的最优解进行转移,即设 \(dp_{i,j}\) 表示第一个数组前 \(i\) 和第二个数组前 \(j\) 的最优解。
- 对于两个连续字符变换为一个字符的关系,不妨将其扩展成一段区间变换为一个字符的关系,这样就能愉快的使用区间DP来求解了。
思路如何生成:
首先观察到,给出的转换关系其实是两个连续的字符可以转换为另一个,注意到 \(n\leq50\),所以考虑将转换关系进一步拓展,拓展为连续的区间转换为一个字符,可以区间DP处理。之后考虑如何获取答案,使用经典双区间套路,设出来 \(dp_{i,j}\) 就可以 \(O(26n^{4})\) 愉快解决了。
调试注意事项:
- 字符转化为数字减去'a'。
- DP数组记得初始化。
CF126D
Difficulty:2300 Tag:斐波那契数列性质+DP
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1010;
int t;
ll n;
stack<ll> fib;
vector<int> a;
vector<int> pos;
ll dp[N][2];
void get(ll x){
a.clear(),pos.clear();
while(fib.size()){fib.pop();}
ll t1=1,t2=2;
fib.push(1),fib.push(2);
while(t2<x){
ll tmp=t1+t2;
t1=t2,t2=tmp;
fib.push(t2);
}
ll tmp=x;
while(fib.size()){
//cout<<fib.top()<<' ';
// fib.pop();
if(fib.top()<=tmp){
tmp-=fib.top();
a.push_back(1);
}
else{
if(a.size()){
a.push_back(0);
}
}
fib.pop();
}//cout<<'\n';
reverse(a.begin(),a.end());
pos.push_back(0);
for(int i=0;i<a.size();++i){
//cout<<a[i];
if(a[i]==1){
pos.push_back(i+1);
}
}//cout<<'\n';
// for(int i=0;i<pos.size();++i){
// cout<<pos[i]<<' ';
// }cout<<'\n';
}
int main(){
ios::sync_with_stdio(false);
cin.tie(NULL),cout.tie(NULL);
cin>>t;
while(t--){
cin>>n;
get(n);
memset(dp,0,sizeof(dp));
dp[1][0]=1;
dp[1][1]=(pos[1]-1)/2;
for(int i=2;i<pos.size();++i){
dp[i][0]=dp[i-1][0]+dp[i-1][1];
dp[i][1]=dp[i-1][0]*((pos[i]-pos[i-1]-1)/2)+dp[i-1][1]*((pos[i]-pos[i-1])/2);
}
cout<<dp[pos.size()-1][0]+dp[pos.size()-1][1]<<'\n';
}
return 0;
}
最主要的就是想到利用齐肯多夫拆分,说白了就是将 \(n\) 一直减去能够减去的最大的斐波那契数,选中的为1,跳过的为0,组成一个01串(数量最小的拆分)。观察01串,发现一定不会有相邻的1,然后发现可以将一个1拆分成两个1代替前面的两个0,对应的就是将一个斐波那契数拆成前面两个数的和,从而得到一个新的方案。考虑对于每一个原拆分中的1从前面的1来转移,手动模拟发现对于000001的情况,发现可以产生的新的拆分的方案数就是0的个数除以2向下取整。变化如下:000001->000110->011010,每次拆掉一个1都会使前一位不能再拆,一直为1,只有再前一位才能继续拆,说白了每次拆都会使能拆的1前进两位,拆分方案即为 \(cnt_0/2\)(向下取整)。
然后自然想到DP状态设计:\(dp_{i,0/1}\) 表示拆/不拆第i个1的方案数。转移的时候注意如果拆掉了之前的1就会多贡献一个0即可。加上预处理每个1的位置,总时间复杂度为 \(O(88n)\)。
思路如何生成:
重要的知识点:齐肯多夫拆分,也就是说一个自然数一定能被拆成若干个斐波那契数的和。并且,一直减最大数的拆分方案一定是所有拆分方案中,所含的斐波那契数数量最少的方案。
对于按位决策问题,直接设计针对每位是否操作的状态即可。
调试注意事项:
- vector和stack多测要清空,dp数组也要初始化。
- 反复核对转移方程正确性。
- 对于除什么需要向下取整的情况,要把除法部分用括号括起来!!!!
dp[i][1]=dp[i-1][0]*((pos[i]-pos[i-1]-1)/2)+dp[i-1][1]*((pos[i]-pos[i-1])/2);
XCF316D123
Difficulty:2300/2300/2400 Tag:神秘DP
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int mod=1e9+7,N=1e6+10;;
int n,cnt=0;
ll dp[N],ans=1;
int main(){
ios::sync_with_stdio(false);
cin.tie(NULL),cout.tie(NULL);
cin>>n;
for(int i=1;i<=n;++i){
int tmp;cin>>tmp;
if(tmp==1){cnt++;}
}
for(int i=n;i>cnt;--i){
ans=1ll*ans*i%mod;
}
dp[0]=dp[1]=1;
for(int i=2;i<=cnt;++i){
dp[i]=(dp[i-1]+1ll*dp[i-2]*(i-1)%mod)%mod;
}
cout<<dp[cnt]*ans%mod<<'\n';
return 0;
}
先交一个题解打个标记,之后再好好想(2025.1.12)。
1.13
CF5E
CF link
difficulty:2400
tag:单调栈
是否借鉴题解思路:是
是否借鉴题解写法:否
调试时间:~45min
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int n,m=0,a[N],b[N],pos=0,maxn=0;
long long ans=0;
stack<pair<int,int> > stk;
bool st[N]={false};
int main(){
ios::sync_with_stdio(false);
cin.tie(NULL),cout.tie(NULL);
cin>>n;
for(int i=1;i<=n;++i){cin>>a[i];}
for(int i=1;i<=n;++i){
if(a[i]>maxn){
pos=i;
maxn=a[i];
}
}
for(int i=pos;i<=n;++i){
b[++m]=a[i];
}
for(int i=1;i<pos;++i){
b[++m]=a[i];
}
for(int i=1;i<=n;++i){
pair<int,int> pii={i,1};
while(stk.size()&&b[stk.top().first]<=b[i]){
ans+=stk.top().second;
if(b[i]==b[stk.top().first]){
pii.second+=stk.top().second;
}
stk.pop();
}
if(stk.size()){
if(stk.top().first==1){
st[i]=true;
}
ans++;
}
stk.push(pii);
}
int maxn=0;
for(int i=n;i>=1;--i){
maxn=max(maxn,b[i]);
if(b[i]>=maxn&&!st[i]&&b[i]!=b[1]){
ans++;
}
}
cout<<ans<<'\n';
return 0;
}
其实就是P1823的加强版,变成了环状而已。要点依旧在于特殊处理相等的情况,本质上还是合并的操作。至于对于环的处理,将最大值提到第一个,随后对整个序列跑一边P1823,这样可以统计完全所有不是最大值的数的对数。之后发现只剩下了后面的一些可以跟最前面的最大值配对的数,这样一个数也一定不是最大值,所以我们只需要在前面link的时候标记一下,link完之后再从后往前扫一次,如果一个数能跟第一个最大值再link(之前没有link过),就ans++。
关键点:
- 单调栈处理看见对数问题
- 破环为链(拆分法,以最大值为分界线拆分再拼接)
- 分类讨论,特殊处理
调试细节:
- 注意在处理到相等的数的时候是不会进入stk.size()的特判的,所以并不会标记st,之后再处理的时候,因为所有最大值之间一定被link过了,所以简单的判断是否为最大值即可。
思路如何生成:
首先考虑经典的破环为链,发现最大值两边的两个部分一定是不会跳过最大值互相配对的,所以将前面的一个部分和后面的一个部分合并,将最大值放在最前面就可以打包计算完全除了最大值以外的配对。
单调栈
维护一个元素单调上升/下降的栈来维护一个元素前/后第一个比他大/小的值。
模板题:
P5788
P1901
P1823(统计能看见的对数的问题)
CF5E
1.14
CF9E
CF link
difficulty:2300
tag:并查集
是否借鉴题解思路:是(之前自己的思路调试未果)
是否借鉴题解代码:否
想法+调试时间:30+60+30+30(太长了)
#include<bits/stdc++.h>
using namespace std;
#define pii pair<int,int>
const int N=60;
int n,m,deg[N],fa[N];
inline int find(int x){
return fa[x]==x?x:fa[x]=find(fa[x]);
}
int cnt=0;
pii ans[N];
int main(){
ios::sync_with_stdio(false);
cin.tie(NULL),cout.tie(NULL);
cin>>n>>m;
if(n<m){
cout<<"NO\n";
return 0;
}
for(int i=1;i<=n;++i){
fa[i]=i;
}
for(int i=1;i<=m;++i){
int u,v;
cin>>u>>v;
deg[u]++,deg[v]++;
fa[find(u)]=find(v);
}
for(int i=1;i<=n;++i){
if(deg[i]>2){
cout<<"NO\n";
return 0;
}
}
for(int i=1;i<=n;++i){
for(int j=i+1;j<=n;++j){
if(find(i)!=find(j)&°[i]<2&°[j]<2){
deg[i]++,deg[j]++;
fa[find(i)]=find(j);
ans[++cnt]={i,j};
}
}
}
if(n-cnt-m==1){
int id=0,pr[2]={0,0};
for(int i=1;i<=n;++i){
if(deg[i]<2){
pr[id++]=i;
}
}
if(id==1){
pr[1]=pr[0];
}
ans[++cnt]={pr[0],pr[1]};
}
for(int i=1;i<=n;++i){
if(find(i)!=find(1)){
cout<<"NO\n";
return 0;
}
}
cout<<"YES\n";
cout<<cnt<<'\n';
for(int i=1;i<=cnt;++i){
cout<<ans[i].first<<' '<<ans[i].second<<'\n';
}
return 0;
}
认为这道题的难点在于如何按照字典序输出。我们考虑对于DFS写法,处理出来了所有的链的端点,此时,我们需要去考虑有两个端点的链和有一个端点的点,会比较难以考虑。我就是因为这个写法调了半天,加了一堆特判还没过。
所以我们不妨直接从本质出发,所有度数小于2的点一定是要参与连线的,所以按照字典序枚举,每次如果两个点需要被连且不连通(用并查集维护连通性),我们就将其加入答案。最后,我们可能会剩下(因为可能原本就是环)两个点,再连起来即可。
这道题细节比较多:
- 判断no的情况要全面:m>n,自环和重边都可以再过程中判断出来(度数)
- 要考虑单个点的情况,其实就是一个 1 0 的情况。
关键点:
- 并查集判断连通性。
- 度数判断是否是链。
- 分类讨论、细节。
调试:
- NO和YES注意大小写要求。
- 记得初始化fa
总结:
- 仔细观察题目限制:1.n的范围告诉我们可以n^2,所以不用去追求n的做法。2.在做图论题的时候要观察题目所给出的图的限制,比如:是有向图还是无向图、n和m的关系(有可能m>n)、连通性、是否有重边、是否有自环、以及一些其它特征。
- 多手造样例方便调试。
CF22E
CF link
difficulty:2300
tag:DFS,图论
是否借鉴题解思路:否(或者说是)
是否借鉴题解实现:是(那个DFS的部分太妙)
实现时间:思考30min+调试40min(下次一定要精准计时和预估)
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
#define pii pair<int,int>
int n,nxt[N];
int id[N]={0},deg[N],cnt=0;
pii a[N];
inline int dfs(int now){
id[now]=1;
if(id[nxt[now]]==0){return id[now]=dfs(nxt[now]);}
else{return id[now]=now;}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(NULL),cout.tie(NULL);
cin>>n;
for(int i=1;i<=n;++i){
cin>>nxt[i];
deg[nxt[i]]++;
}
//先把所有有开头的点都给弄出来,并标记已经弄过的点
int cnt0=0,cnt1=0;
for(int i=1;i<=n;++i){
if(deg[i]==0){
a[++cnt]={i,dfs(i)};
cnt0++;
}
}
for(int i=1;i<=n;++i){
if(id[i]==0){
a[++cnt]={i,dfs(i)};
cnt1++;
}
}
if(cnt1==1&&cnt0==0){
cout<<"0\n";
}
else{
cout<<cnt<<'\n';
cout<<a[cnt].second<<' '<<a[1].first<<'\n';
for(int i=1;i<cnt;++i){
cout<<a[i].second<<' '<<a[i+1].first<<'\n';
}
}
return 0;
}
思路其实是不难的,我们从题目中获得关键信息就是每个点都有且仅有一条出边。那么对于一个联通块(在无向图意一下联通)来说,一定是一个环的形式或者一个环和若干条链的形式,且链都是指向环的。不难发现,我们可以把环视作一个没有出度的结点,之后我们将所有没有出度的节点也就是链的尾部和所有没有入度的节点顺次相接,即可保证图联通。说起来抽象,画几个图就非常明了。
所以说整个算法可以分成两部分:
- 从入度为0的结点出发,找到一条“链”,链的尾端是一个环,我们只需要用环的一个节点来代表尾端的环即可,因为与环中任意节点链接是等价的。
- 找到所有剩余的单独的环,并将它们认为是环中的一个节点到它本身的链,方便后面首位相连。
- 首位相连的输出所有的配对即可。
难点在于寻找“链”的部分。朴素的思路是用vis数组来表示是否访问过,如果再次遇到已经访问过的,就说明找到了一个环的起始点。但是这种思路是不行的,因为有可能两个链有公共部分,在上一次标记之后下一次寻找就不能找上去了。所以我们不妨给点增加一个vis信息,等于0是表示没有访问过,等于一个数时,表示这个链上的环的一个节点,这样下一次访问的时候直接取值即可。
最主要思路如何生成:
- dfs的过程可以包含回溯,节点上可以存储相关信息。
XCF24E
CF link
difficulty:2300
tag:分数规划,二分
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
int n,dir[N];
struct node{
int a,b;
}a[N];
int cmp(node n1,node n2){
return n1.a<n2.a;
}
int check(double mid){
double res=-1e20;
for(int i=1;i<=n;++i){
if(dir[i]){
res=max(res,a[i].a+mid*a[i].b);
}
else if(a[i].a+mid*a[i].b<=res){
return 1;
}
}
return 0;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(NULL),cout.tie(NULL);
cin>>n;
for(int i=1;i<=n;++i){
cin>>a[i].a>>a[i].b;
dir[i]=(a[i].b>0)?1:0;
}
double l=0,r=1e10;
for(int i=1;i<=100;++i) {
double mid=(l+r)/2;
if(check(mid)){r=mid;}
else{l=mid;}
}
if(r==1e10){
cout<<"-1\n";
}
else{
cout<<fixed<<setprecision(15)<<l<<'\n';
}
return 0;
}
XXX
1.15
CF36D
CF link
difficulty:2300
tag:DP打表 找规律
思考预估:8:30-9:30//10:00-10:10
实现预估:30min//~25min
#include<bits/stdc++.h>
using namespace std;
const int N=30;
int t,k,n,m;
bool dp[N][N];
int main(){
freopen("input.txt","r",stdin);
freopen("output.txt","w",stdout);
ios::sync_with_stdio(false);
cin.tie(NULL),cout.tie(NULL);
cin>>t>>k;
// dp[1][1]=0;
// for(int i=1;i<=n;++i){
// for(int j=1;j<=n;++j){
// bool b1=false,b2=false,b3=false;
// if(i-1>=1){
// b1=(dp[i-1][j]);
// }
// else{
// b1=1;
// }
// if(j-1>=1){
// b2=(dp[i][j-1]);
// }
// else{
// b2=1;
// }
// if(i-k>=1&&j-k>=1){
// b3=(dp[i-k][j-k]);
// }
// else{
// b3=1;
// }
// dp[i][j]=!(b1&&b2&&b3);
// //cout<<i<<' '<<j<<' '<<b1<<','<<b2<<','<<b3<<','<<(b1&&b2&&b3)<<dp[i][j]<<'\n';
// }
// }
// for(int i=1;i<=n;++i){
// for(int j=1;j<=n;++j){
// cout<<dp[i][j];
// }
// cout<<'\n';
// }
// cout<<'\n';
while(t--){
cin>>n>>m;
if(n%(k+1)==0&&m>(n/(k+1)*k+n/(k+1)-1)){
cout<<"+\n";
continue;
}
if(m%(k+1)==0&&n>(m/(k+1)*k+m/(k+1)-1)){
cout<<"+\n";
continue;
}
int dep=min(n/(k+1),m/(k+1));
//cout<<dep<<'\n';
int sw=dep*(k+1);
int nx=n-sw,mx=m-sw;
if(dep%2==0||k==1){
if((nx+mx)%2==0){
cout<<"-\n";
}
else{
cout<<"+\n";
}
}
else if(dep%2==1){
if((nx+mx)%2==0){
cout<<"+\n";
}
else{
cout<<"-\n";
}
}
}
return 0;
}
终于找到一道正宗的打表找规律题了。
首先考虑暴力的做法:设每个格子上先手能否必胜为 \(dp_{i,j}\),则转移方程为 \(dp_{i,j}=!(dp_{i-1,j}⋀dp_{i,j-1}⋀dp_{i-k,j-k})\),不存在的时候设为true不影响即可。在此处,我们就能够知道博弈问题的一般DP方式:从更小的规模转移,对于这种谁走不了谁输的情况,如果所有能到达这个状态的状态都是赢,那么这个状态一定是输,否则一定是赢。该学学博弈论了。必胜一定是最有策略,不走最优策略一定更劣。
但是我们发现,这个玩意需要O(1)查询,下面就有两种方式了:
1.直接把DP的表弄出来找规律,然后直接模拟即可。但是此时会发现,交上去祭在了k=1的点,把k=1的表打出来发现每个L形中间并没有变换01的顺序,特殊处理即可。
2.继续想博弈论的方法(这个我没想到)
这道题提醒我们,在打表找规律的时候,一定要看一下这个打表的本质是什么,可能出现什么样的特殊情况,按顺序多打一点,这样就不会忽略k=1的情况了。
调试:
- if elseif 体现优先级,尽量不要连续if。
OCF37D
CF link
difficulty:2300
tag:****
OCF38G
CF link
difficulty:2300
1.16
CF50E
CF link
difficulty:2300
tag:数学 差分
是否借鉴题解思路:否
是否借鉴题解实现:否
实现时间:思考~40min + 调试~28min(下次一定要精准计时和预估)
RE2 AC1
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=2e7+10;
int n,m,a[N]={0};
const int eps=10000000;
ll ans=0;
int main(){
ios::sync_with_stdio(false);
cin.tie(NULL),cout.tie(NULL);
cin>>n>>m;
for(int b=1;b<=n;++b){
ans+=min(1ll*m,1ll*b*b)*2;
int low=int(ceil(sqrt(1.00*max(0.00,1.00*b*b-m))));
int high=int(sqrt(1.00*b*b-1.00));
a[-b+low+eps]+=1;
a[-b+high+1+eps]-=1;
a[-b-high+eps]+=1;
a[-b-low+1+eps]-=1;
}
for(int i=1;i<=n*2+eps;++i){
a[i]+=a[i-1];
}
int cnt=0;
for(int i=0;i<=n+eps;++i){
if(a[i]>1){
cnt+=a[i]-1;
}
}
cout<<ans-cnt<<'\n';
return 0;
}
好玩的数学题。
首先,在b,c都是整数的情况下,重复的根一定是形如\(x = k\in \mathbb{Z}\) 的,因为形如 \(x=p+\sqrt{q}\) 的根只有在 \(p,q\) 都相等的时候才会相等。在 \(x = -b\pm\sqrt{b^{2}-c}\) 的情况下一定不会重复。再观察一下,很容易发现 \(1\leq c \leq \min(m,b^{2})\)。
下面观察数据范围,发现可以接受 \(O(n)\),所以我们不妨枚举 \(b\),然后统计每个 \(b\) 的整数根都会落在那个区间里,区间的值域是 \(1e7\) 级别的,可以接受。
不妨设 \(b^{2}-c=k^{2},k\in \mathbb{Z}\),则有 \(\lceil \sqrt{b^{2}-\min(m,b^2)} \rceil \leq k \leq \lfloor \sqrt{b^{2}-1} \rfloor\)。
所以对于一个 \(b\),其所有的整数根应该是:
\(x_1=-b+k, x_1\in[-b+\lceil \sqrt{b^{2}-\min(m,b^2)} \rceil,-b+\lfloor \sqrt{b^{2}-1} \rfloor]\)
\(x_2=-b-k, x_2\in[-b-\lfloor \sqrt{b^{2}-1} \rfloor,-b-\lceil \sqrt{b^{2}-\min(m,b^2)} \rceil]\)
由于 \(1 \leq b \leq 5*10^{6}\),所以根的值域应该是正负 \(10^{7}\) 的,利用差分数组来区间修改,最后 \(O(n)\) 查询一次即可。
调试注意事项:
- eps的选取和整个数组的值域大小需要考虑,即考虑所有作为下标的式子的取值范围。
- 注意在范围里出现浮点数变成整数的时候,考虑是否要向上取整(例如k的范围)。
- 差分数组:\(a_l+=k,a_{r+1}-=k\),注意加一。
O-CF311E
CF link
difficulty:2300
tag:网络流
11:05开始思考。
11:20先去学习网络流。
打个标记,学完网络流回来做。
O-CF317D
CF link
difficulty:2300
tag:SG函数、博弈论
14:02开始思考。
不会SG函数,还是学完回来做。打个标记
1.18
O-CF167C
CF link
difficulty:2300
tag:博弈论
1.16:
14:27开始思考。
放到明(后)天吧qaq,先上点ACwing。
1.18:
0835开始思考。
好好好,又是博弈论,直接开题解。
题解看不懂,之后不选博弈论了。
有时间学完博弈论再回来给这些题负责。
O-CF915G
CF link
difficulty:2300
tag:数论
1037开始思考
莫比乌斯变换,放弃。之后再负责。
O-CF444B
CF link
difficulty:2300
tag:****
1108开始思考
人类智慧+各种乱搞,算了再弃。
但是这道题之后必须补。
1.19
CF1790G
CF link
difficulty:2300
tag:图论,BFS
思路是1.18晚上推的。
1.19早上9:00开始实现,调到了10:00然后10:30再继续调,11:09才AC,太慢了,真的不好。
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int t,n,m,p,b,cnt[N],dis[N],cntt=0,tot=0;
bool tok[N],pri[N],vis[N];
vector<int> g[N];
vector<int> token;
struct node{
int id;
int dep;
};
void init(){
tot=cntt=0;
for(int i=0;i<=n;++i){
cnt[i]=dis[i]=tok[i]=pri[i]=vis[i]=0;
g[i].clear();
}
// memset(cnt,0,sizeof(cnt));
// memset(dis,0,sizeof(dis));
// memset(tok,false,sizeof(tok));
// memset(pri,false,sizeof(pri));
// memset(vis,false,sizeof(vis));
// for(int i=0;i<N;i++){g[i].clear();}
token.clear();
}
void bfs(){
queue<node> q;
q.push({1,0});
vis[1]=true;
while(q.size()){
node top=q.front();
q.pop();
vis[top.id]=true;
for(auto v:g[top.id]){
if(dis[v]==0&&tok[v]){
dis[v]=top.dep+1;
}
if(pri[v]){
if(!vis[v]){
q.push({v,top.dep+1});
}
}
}
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(NULL),cout.tie(NULL);
cin>>t;
while(t--){
cin>>n>>m>>p>>b;
init();
bool tag=false;
for(int i=1;i<=p;++i){
int x;
cin>>x;
tok[x]=true;
token.push_back(x);
}
for(int i=1;i<=b;++i){
int x;
cin>>x;
pri[x]=true;
}
for(int i=1;i<=m;++i){
int u,v;
cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
if(tok[1]){
tag=true;
}
for(auto u:token){
if(pri[u]){
// cout<<u<<'\n';
bool flg0=false;
for(auto v:g[u]){
if(pri[v]){
flg0=true;
break;
}
}
if(flg0){cnt[u]=-1;cntt++;/*cout<<"typ1\n";*/}
else{cnt[u]=0;}
}
else{
bool flg1=false,flg2=false;
for(auto v:g[u]){
if(pri[v]){
flg1=true;
if(flg2==true){continue;}
for(auto k:g[v]){
if(pri[k]){
flg2=true;
break;
}
}
}
}
if(!flg1&&!flg2){cnt[u]=0;}
else if(flg1&&!flg2){cnt[u]=1;tot++;}
else if(flg1&&flg2){cnt[u]=-1;cntt++;/*cout<<"typ2\n";*/}
}
}
//cout<<"cute.\n";
bfs();
// for(int i=1;i<=n;++i){
// cout<<dis[i]<<' ';
// }cout<<'\n';
// for(int i=1;i<=n;++i){
// cout<<cnt[i]<<' ';
// }cout<<'\n';
bool flg=false;
// cout<<cntt<<'\n';
for(auto u:token){
if(dis[u]==1){
flg=true;
continue;
}
if(dis[u]==0){continue;}
bool flg11=false;
if(cnt[u]==-1){flg11=(cntt-1>0);}
else{flg11=(cntt>0);}
int tmp=tot;
if(cnt[u]==1){tmp--;}
if(flg11||tmp>=dis[u]-1){
flg=true;
// cout<<flg11<<'\n';
// cout<<tmp<<'\n';
// cout<<u<<','<<dis[u]<<'\n';
break;
}
}
if(flg||tag){cout<<"YES\n";}
else{cout<<"NO\n";}
init();
}
return 0;
}
感觉这道题的难度全在实现(调试)上,一遍过虽然也可能但是也就这样了。
还是说一下思路吧,就是你会发现,这个能够获胜的令牌一定是在全是奖励的一条路上,假设我们想让一个令牌去获胜,那么,剩下的所有令牌的移动次数总和一定要大于这个令牌到1点的最短路。最短路可以BFS,一个令牌的最多移动次数可以分成0、1和无数次(因为可以走回头路),处理出来然后就做完了。
下面是调试过程以及总结:
写完。
样例单独测都能过,但是放在一块就不行,说明是初始化的问题,调了半天初始化,没发现问题,结果发现是本应该是int类型的cntt和tot定义在bool后面了,改了。
但是还是过不了,发现是特判令牌直接满足的情况的continue把后面的初始化和输入都吞了,于是把写成了单个出口(每个while里没有continue,输出都放在一块)。然后WA#6。
(用了很多时间去趴CF数据,但是最终还不如自己调)
调了半天发现flg2==true写成了flg2=true。不好评价。
所以总结一下调试心得:
- 多测建议吧输入都写在一块,尽量不要continue,防止吞掉清空部分。(有时候需要在每次之后清空。)
- 写代码可以写慢一点,特别是长的代码,写一部分调一部分,边写边调,再加上注释,可以降低出错概率。
- 数据类型的定义一定要看好。
CF1977D
CF link
difficulty:2300
tag:哈希(字符串、双模数),map
13:14开始思考14:31AC
MLE2 TLE1 WA1 AC
中途参照了些许题解的思路。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define pll pair<ll,ll>
const int N=3e5+10;
const int base1=1331,base2=131;
const int m1=998244353,m2=1e9+7;
int t,n,m,ans=0;
string a[N],str;
map<pll,int> mp;
ll quick_power(ll a,ll b,int p){
ll ret=1;
while(b){
if(b&1){
ret=ret*a%p;
}
a=a*a%p;
b>>=1;
}
return ret;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(NULL),cout.tie(NULL);
cin>>t;
while(t--){
mp.clear();
ans=0,str="";
cin>>n>>m;
for(int i=1;i<=n;++i){cin>>a[i];}
for(int j=0;j<m;++j){
string tmp="";
for(int i=1;i<=n;++i){
tmp+=a[i][j];
}
ll h1=0,h2=0;
for(int i=0;i<n;++i){
h1=(1ll*h1*base1+1ll*(tmp[i]-'0'))%m1;
h2=(1ll*h2*base2+1ll*(tmp[i]-'0'))%m2;
}
for(int i=0;i<n;++i){
if(tmp[i]=='1'){tmp[i]='0';h1=(h1-quick_power(base1,n-i-1,m1)+m1)%m1;h2=(h2-quick_power(base2,n-i-1,m2)+m2)%m2;}
else if(tmp[i]=='0'){tmp[i]='1';h1=(h1+quick_power(base1,n-i-1,m1)+m1)%m1;h2=(h2+quick_power(base2,n-i-1,m2)+m2)%m2;}
pll kk=make_pair(h1,h2);
mp[kk]++;
if(mp[kk]>ans){
ans=mp[kk];
str=tmp;
}
if(tmp[i]=='1'){tmp[i]='0';h1=(h1-quick_power(base1,n-i-1,m1)+m1)%m1;h2=(h2-quick_power(base2,n-i-1,m2)+m2)%m2;}
else if(tmp[i]=='0'){tmp[i]='1';h1=(h1+quick_power(base1,n-i-1,m1)+m1)%m1;h2=(h2+quick_power(base2,n-i-1,m2)+m2)%m2;}
}
}
cout<<ans<<'\n'<<str<<'\n';
}
return 0;
}
首先,第一个观察是:
- 使(i,j)成为这一列中唯一的1的方案是唯一的
这个观察的普遍得来思路就是我们先不考虑整体,先考虑最小的元素,先确定最小的元素。或者说,操作是对行的,要求是对列的,所以从同时拥有行、列两个特征的元素,也就是坐标开始考虑。
然后我们发现 \(n\times m \leq 3\times 10^{5}\),所以可以枚举每个坐标。然后我们发现,对于每一列中的所有点,都是在同一个01序列上做修改。所以可以枚举列,然后对于这一列从上到下递推,每个元素都是改变整个列的01序列上对应的01值。
之后,我们只需要用一个map来存储某个01序列被取到了多少次然后取最大值就行了,就是说这个01序列能够满足多少个点。
但是,我们会发现,如果直接 map<string,int> mp 会MLE。所以我们要考虑将字符串哈希。但是,如果单独对于每个01方案字符串哈希,时间复杂度最坏将会是 \(O(n^2m)\) 的,不能接受,考虑到之前的递推方案,每次修改原01序列的哈希值就可以了。因为字符串哈希的本质是高进制数,在取模的意义下,每次修改时减掉或者加上一个位的值即可。某个位的值的形式是类似于 \(0/1 \times base^{n-k}\) 的。
从这道题中学到以下东西:
- 需要考虑复杂度,时间复杂度和空间复杂度都要考虑。
- 在取模意义下,对于大进制数可以直接加上或者减去它的一位。
- 研究对象非常重要(考虑到确定坐标),找共同点也很重要(考虑对于每列递推)。
O-CF1949G
CF link
1500-1522思考-1
1550-1600思考-2
之后再来补叭~
1.21 8:50-(调试)

浙公网安备 33010602011771号