星期一:
补牛客周赛39 D 牛客传送门
思路:这题数据不够强,dp【i】表示价值总和为 i 的物品最少数量,跑到100*p的价值,取p到100p中最少物品数,能过
正解是bfs,map存的是单个物品价值,vi存的是bfs到达的点,每次跑一遍p,能去但没去的点就继续存入队列
代码如下:
const int N=2e6+10,M=210;
const int mod=1e9+7;
ll n;
int p;
int a[N];
bool vi[N];
void solve(){
cin >> n >> p;
queue<PII>qu;
map<int,int>mp;
for(int i=1;i<=n;i++){
int x; cin >> x;
if(!vi[x%p]){
qu.push({x%p,1});
vi[x%p]=1; //对到过的点进行标记
mp[x%p]=1; //对物品价值进行标记
}
}
while(1){
auto [x,y]=qu.front(); qu.pop();
if(!x){
cout << y;
return ;
}
for(int i=0;i<p;i++){
if(!mp[i] || vi[(x+i)%p]) continue;
vi[(x+i)%p]=1;
qu.push({(x+i)%p,y+1});
}
}
}
补牛客周赛round40 D 牛客传送门
思路:dp【i】表示花费 i 金币可达到的最大战力
刚开始不知道购买和升级的关系怎么处理
多重背包,前两维正常枚举,第三维枚举升级数,顺带着购买金币p【i】和初始战力a【i】就行了
代码如下:
ll n;
int x;
int a[330],p[330],c[330],u[330],l[330];
ll dp[330];
void solve(){
cin >> n >> x;
for(int i=1;i<=n;i++)
cin >> a[i] >> p[i] >> c[i] >> u[i] >> l[i];
for(int i=1;i<=n;i++){
for(int j=x;j>=p[i];j--)
for(int k=0;k<=l[i] && k*c[i]<=j-p[i];k++) //枚举升多少级
dp[j]=max(dp[j-p[i]-k*c[i]]+a[i]+k*u[i],dp[j]);
//for(int k=min(l[i],(j-p[i])/c[i]);k>=0;k--) //k倒着枚举也行
//dp[j]=max(dp[j-p[i]-k*c[i]]+a[i]+k*u[i],dp[j]);
}
cout << dp[x];
}
dp题单 数位dp第四题 cf传送门
思路:很刁的一道数位dp 参考题解
dp【now】【num】【lcm】表示当前枚举到了数字的第now位,枚举到上一位的数是num,所有枚举数字的最小公倍数为lcm,dfs的第四个参数 if1表示目前枚举是否在数字的上界
首先解释下dp数组的维度大小,第一维是数字长度,不超过20
第二位是数字大小,因为1-9的公倍数为2520,所以 结果数%公倍数,和结果数先模2520,再%公倍数的结果是一样的,所以边累加边对2520取模
第三维是枚举数字的公倍数,1-9的公倍数是2520,可能会mle,实际上2520的因数只有50个不到,所以可以开个数组或mp映射一下,第三维开50就够了
然后要注意的一点是,记忆化处理时要注意上界状态,只有非上界状态才能记忆化处理
代码如下:
ll n;
ll dp[20][2522][55];
int lc[2522],a[20],cnt;
ll dfs(int now,int num,int lcm,bool if1){
if(!now){
if(num%lcm==0) return 1;
return 0;
}
if(!if1 && dp[now][num][lc[lcm]]!=-1) //如果在上界状态,不能直接结算
return dp[now][num][lc[lcm]]; //记忆化处理
int ma=9; //当前位枚举的数字
if(if1) ma=a[now]; //若在上界,则不得超过a[now]
ll res=0;
for(int i=0;i<=ma;i++)
res+=dfs(now-1,(num*10+i)%2520,i?i*lcm/__gcd(i,lcm):lcm,if1 && i==ma);
if(!if1) dp[now][num][lc[lcm]]=res; //若在上界状态,不能记忆化记录答案
return res;
}
void solve(){
int t; cin >> t;
for(int i=1,j=0;i<=2520/i;i++)
if(2520%i==0) lc[i]=++j,lc[2520/i]=++j;
memset(dp,-1,sizeof dp); //记忆化处理
while(t--){
ll l,r; cin >> l >> r;
ll tl=l-1,tr=r;
cnt=0;
while(tl) a[++cnt]=tl%10,tl/=10;
ll resl=dfs(cnt,0,1,1);
cnt=0;
while(tr) a[++cnt]=tr%10,tr/=10;
ll resr=dfs(cnt,0,1,1);
cout << resr-resl << "\n";
}
}
星期二:
每日构造(bushi cf传送门
题意:将1至n*m的数,填入n*m的方格中,使得相邻方格填入值的差绝对值大于1
思路:首先n若小于m,交换n和m的值,方便判断和思考情况
-1的情况三种,(n==1 && (m==2 || m==3)) || (n==2 && m==2)
然后分为了两种情况构造,n只有一行,先顺着放偶数,再顺着放奇数
n>1,将第一行的偶数放到最后一行的偶数格,其余行偶数放上一行的偶数格
最后输出时,记得判断是否有交换n和m的值
代码如下:
ll n;
ll m;
int a[110][110];
void solve(){
cin >> n >> m;
bool if1=0;
if(n>m) swap(n,m),if1=1;
if(n==1 && m==1){cout << 1; return ;}
if((n==1 && m<=3) || (n==2 && m==2)){cout << -1; return ;}
if(n==1){
for(int j=2;j<=m;j+=2) cout << j << " ";
for(int j=1;j<=m;j+=2) cout << j << " ";
return ;
}
for(int j=1;j<=m;j++){
if(j&1) a[1][j]=j;
else a[n][j]=j;
}
int num=m+1;
for(int i=2;i<=n;i++){
for(int j=1;j<=m;j++){
if(j&1) a[i][j]=num++;
else a[i-1][j]=num++;
}
}
if(!if1){
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++) cout << a[i][j] << " \n"[j==m];
}else{
for(int j=1;j<=m;j++)
for(int i=1;i<=n;i++) cout << a[i][j] << " \n"[i==n];
}
}
同场顺便做了道数据结构: cf传送门
思路:模拟链表+优先队列
用链表来模拟删除操作,第一次把挨着的BG全丢队列里,然后用优先队列跑,离队时,链表删除两节点,若接上的是BG,继续丢队列里
代码如下:
const int N=2e6+10,M=210;
const int mod=1e9+7;
ll n;
string s;
int a[N],pre[N],nex[N];
bool vi[N];
struct nod{
int idl,idr,ab;
bool operator < (const nod &a) const{ //重载运算符
return a.ab==ab?a.idl<idl:a.ab<ab;
}
};
void solve(){
cin >> n >> s;s=" "+s;
for(int i=1;i<=n;i++){
cin >> a[i];
pre[i]=i-1;
nex[i]=i+1;
}
priority_queue<nod>pq; //优先队列维护差值最小的一对
for(int i=1;i<n;i++)
if(s[i]!=s[i+1]) pq.push({i,i+1,abs(a[i]-a[i+1])});
queue<PII>ans;
while(!pq.empty()){
auto t=pq.top(); pq.pop();
if(vi[t.idl] || vi[t.idr]) continue; //若两人中已有人离队,跳过
ans.push({t.idl,t.idr});
vi[t.idl]=vi[t.idr]=1;
nex[pre[t.idl]]=nex[t.idr];
pre[nex[t.idr]]=pre[t.idl]; //链表的删除操作
if(pre[t.idl]<1 || nex[t.idr]>n) continue;
if(s[pre[t.idl]]!=s[nex[t.idr]]) //若重拼接的俩字符符合条件
pq.push({pre[t.idl],nex[t.idr],abs(a[pre[t.idl]]-a[nex[t.idr]])});
}
cout << ans.size() << "\n";
while(!ans.empty()){
auto [x,y]=ans.front(); ans.pop();
cout << x << " " << y << "\n";
}
}
下午天梯模拟赛,还行,竟然还能复习下kmp板子
星期三:
dp题单 数位dp第五题 cf传送门
思路:dp【i】【j】【k】【y】表示考虑到第 i 位数,目前数字%m后为 j,奇偶,前面是否一直为0
dfs多了一个参数 ifm 表示是否为上界状态
这里给出的a和b为字符串形式,不采用让a-1的方案,这涉及高精度,以及a字符串的长度可能改变,于是直接判断a是否符合条件,符合则最后答案+1
因为填数的要求与位数的奇偶性有关,若前面一直为0,则此位数的奇偶性应该一直为1,且因num不断取模,无法根据num判断前面是否填过数,所以用 ifn判断
然后注意上界状态不能记忆化处理
代码如下:
ll n;
int m,d;
string a,b;
ll dp[2020][2020][2][2];
int c[2020];
ll dfs(int now,int num,bool if1,bool ifm,bool ifn){
if(now>n) return num==0; //%m==0即符合条件
if(!ifm && dp[now][num][if1][ifn]!=-1)
return dp[now][num][if1][ifn];
ll res=0;
if(!ifn) res+=dfs(now+1,0,if1,0,0); //若前面一直为0,可以接着填0,奇偶性不变
if(if1){ //奇数位填数
int ma=9;
if(ifm) ma=c[now];
for(int i=ifn==0;i<=ma;i++){ //填过数,i可以从0开始,否则1开始
if(i==d) continue;
res+=dfs(now+1,(num*10+i)%m,if1^1,ifm&&i==ma,1),res%=mod;
}
}else{ //偶数位填数
if(ifm && c[now]<d) res=0;
else res+=dfs(now+1,(num*10+d)%m,if1^1,ifm&&d==c[now],1),res%=mod;
}
if(!ifm) dp[now][num][if1][ifn]=res; //记忆化处理
return res;
}
void solve(){
cin >> m >> d;
cin >> a >> b;
n=a.size();
a=" "+a,b=" "+b;
bool ifa=1;ll numa=0; //判断a是否符合条件
for(int i=1;i<=n;i++){
if(i&1 && a[i]-'0'==d) ifa=0;
if(!(i&1) && a[i]-'0'!=d) ifa=0;
numa=(numa*10+a[i]-'0')%m;
}
if(numa) ifa=0;
for(int i=1;i<=n;i++) c[i]=a[i]-'0';
memset(dp,-1,sizeof dp); //记忆化处理
ll res1=dfs(1,0,1,1,0); if(ifa) res1--;
for(int i=1;i<=n;i++) c[i]=b[i]-'0';
ll res2=dfs(1,0,1,1,0);
ll ans=(res2-res1+mod)%mod;
cout << ans;
}
atcoder ABC349 D atc传送门
思路:纯贪心
每次分开的区间长度其实就是2的整数次幂,所以直接贪心的放即可
代码如下:
ll n;
void solve(){
ll l,r; cin >> l >> r;
vector<PII>ans;
while(l<r){
ll i=1,j=l;
while(!(j&1) && l+i<=r) j>>=1,i<<=1;
while(l+i>r) i>>=1,j<<=1;
ans.push_back({l,l+i});
l+=i;
}
cout << ans.size() << "\n";
for(auto i:ans) cout << i.first << " " << i.second << "\n";
}
星期四:
每日构造(2200 cf传送门
题意:给定一1到n升序排列的permutation,k,长度为m的数组b
操作为选定k个数,除了这k个数的中位数,其余数删去,问能否通过任意次操作得到数组b
思路:先说结论,若存在 bi 前后都有 大于等于 (k-1)/2 个被删数则YES,否则NO,如何证明还没想明白
dp题单 区间dp第二题 括号涂色(区间dp好题啊 cf传送门
0代表无色,1代表蓝色,2表示红色
思路:dp【l】【r】【i】【j】表示涂好 l 到 r ,l 涂 i 色,r 涂 j 色的方案数
初始化把每个(对应的)下标记录,用stack实现
区间dp用dfs递归实现,因为s的括号必两两对应,所以dfs的 l 和 r 从1,n开始,l 必定是(,r必定是),l,r的对应分三种情况,一是 r==l + 1,直接赋值即可 , 二是 l 和 r 对应,但不相邻,枚举 l+1 和 r-1 的涂色方案,把合法状态加上即可, 三是不对应,则需要把 l 和 ma【l】,ma【l】+1和 r 两个对应的区间的方案算出,两区间方案数相乘
代码如下:
ll n;
string s;
stack<int>sk;
int ma[777];
ll dp[777][777][3][3];
void init(){
for(int i=1;i<=n;i++){
if(s[i]=='(') sk.push(i);
else ma[sk.top()]=i,sk.pop();
}
}
void dfs(int l,int r){
if(l==r-1){
dp[l][r][0][1]=dp[l][r][0][2]=dp[l][r][1][0]=dp[l][r][2][0]=1;
return ;
}
if(ma[l]==r){
dfs(l+1,r-1);
for(int i=0;i<3;i++){
for(int j=0;j<3;j++){
if(j!=1) dp[l][r][0][1]+=dp[l+1][r-1][i][j],dp[l][r][0][1]%=mod;
if(j!=2) dp[l][r][0][2]+=dp[l+1][r-1][i][j],dp[l][r][0][2]%=mod;
if(i!=1) dp[l][r][1][0]+=dp[l+1][r-1][i][j],dp[l][r][1][0]%=mod;
if(i!=2) dp[l][r][2][0]+=dp[l+1][r-1][i][j],dp[l][r][2][0]%=mod;
}
}
}else{
dfs(l,ma[l]);
dfs(ma[l]+1,r);
for(int i=0;i<3;i++){
for(int j=0;j<3;j++){
for(int p=0;p<3;p++){
if(p>0 && p==j) continue;
for(int q=0;q<3;q++){
dp[l][r][i][q]+=dp[l][ma[l]][i][j]*dp[ma[l]+1][r][p][q];
dp[l][r][i][q]%=mod;
}
}
}
}
}
}
void solve(){
cin >> s;
n=s.size(),s=" "+s;
init();
dfs(1,n);
ll ans=0;
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
ans+=dp[1][n][i][j],ans%=mod;
cout << ans;
}
星期五:
这两天都在准备星期六的天梯赛,没啥可放的题
小白月赛91 D:半小时出思路,代码写出来又调了半小时,感觉不该调这么久
思路:想着计算方案数和二进制有关,所以用二进制的思路考虑了下
发现一个规律,当 i 位上为偶数,计算与前面组合的方案时,前面可以看作一个长度 i-1 的二进制数,每位上如果为0就为0,否则为1,那么方案数就能+=此二进制数
因为在组合偶数时,只有0开头的数不能选,为0,j 位数为头 i 位数为尾的方案为2的i-j-1次幂,所以可以用一个变量当作 "二进制前缀和",实现O(1)计算
代码如下:
ll n;
string s;
void solve(){
cin >> n >> s;s=" "+s;
ll ans=0,num=0;
for(int i=1;i<=n;i++){
int t=s[i]-'0';
if(!t){
ans++;
ans+=num,ans%=mod;
num<<=1,num%=mod;
}else if(t&1){
num=num*2+1,num%=mod;
}else{
ans++;
ans+=num,ans%=mod;
num=num*2+1,num%=mod;
}
}
cout << ans;
}
星期六:
pta 森森旅游,一道很新的题 pta链接
思路:需要跑两遍dij,一遍是从1用现金到各点,一遍是从n用旅游金到各点,这样在 i 点换钱所需的现金就是 dis1【i】+disn【i】/ a【i】(除法向上取整,然后把结果丢 multiset 里,在询问时动态维护
代码如下:( 不知哪里扣了1分
const int N=1e5+10,M=210;
const int mod=1e9+7;
ll n;
int m,q;
vector<PII>vec[N],ved[N];
ll disc[N],disd[N],a[N];
bool vi[N];
multiset<ll>ms;
void dij1(int s){
priority_queue<PII,vector<PII>,greater<PII>>pq;
memset(disc,0x3f,sizeof disc);
disc[s]=0;
pq.push({0,s});
while(!pq.empty()){
auto [d,t]=pq.top(); pq.pop();
if(vi[t]) continue;
vi[t]=1;
for(auto [v,w]:vec[t]){
if(disc[w]<=d+v) continue;
disc[w]=d+v;
pq.push({disc[w],w});
}
}
}
void dij2(int s){
priority_queue<PII,vector<PII>,greater<PII>>pq;
memset(disd,0x3f,sizeof disd);
disd[s]=0;
pq.push({0,s});
while(!pq.empty()){
auto [d,t]=pq.top(); pq.pop();
if(vi[t]) continue;
vi[t]=1;
for(auto [v,w]:ved[t]){
if(disd[w]<=d+v) continue;
disd[w]=d+v;
pq.push({disd[w],w});
}
}
}
void solve(){
cin >> n >> m >> q;
while(m--){
int u,v,c,d; cin >> u >> v >> c >>d;
vec[u].push_back({c,v});
ved[v].push_back({d,u}); //反向建图
}
dij1(1);
for(int i=1;i<=n;i++) vi[i]=0;
dij2(n);
for(int i=1;i<=n;i++){
cin >> a[i];
ll c=disc[i]+disd[i]/a[i];
if(disd[i]%a[i]) c++; //向上取整
ms.insert(c);
}
while(q--){
int x,b; cin >> x >> b;
ll c=disc[x]+disd[x]/a[x];
if(disd[x]%a[x]) c++;
ms.erase(ms.find(c)); //删除原来的,再把新的插进去
a[x]=b;
c=disc[x]+disd[x]/a[x];
if(disd[x]%a[x]) c++;
ms.insert(c);
cout << *(ms.begin()) << "\n"; //输出最小值
}
}
天梯赛237,国二,洋洋得意
又可以爽刷dp,构造咯
atcoder abc350 C 给我绕晕了 atc传送门
思路:模拟交换即可,只是要注意 思路保持清晰
代码如下:
const int N=2e6+10,M=210;
const int mod=1e9+7;
ll n;
int a[N],c[N];
void solve(){
cin >> n;
for(int i=1;i<=n;i++){
cin >> a[i];
c[a[i]]=i; //c储存各个数的下标位置
}
int num=0;
vector<PII>ve;
for(int i=1;i<=n;i++){
if(a[i]==i) continue;
num++;
int idx=c[i]; //i数下标为idx
ve.push_back({i,idx});
swap(a[i],a[idx]);
c[a[idx]]=idx; //交换后更新下标信息
}
cout << num << "\n";
for(auto [x,y]:ve) cout << x << " " << y << "\n";
}
周日:
组队赛训练,又开始坐牢