一些并不怎么重要的题目
P1111 修复公路
肖邦 g 小调第一叙事曲
并查集 + \(krustra\) 板子题
code:
#include<bits/stdc++.h>
using namespace std;
const int maxn=1010;
const int maxm=100100;
int n,m;
int en=0;
int ans=-1;
int to[maxn];
inline int read()
{
int f=1,x=0;char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-')f=-1;ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=x*10+ch-48;
ch=getchar();
}
return f*x;
}
struct edge{
int p1,p2,d;
}ed[maxm];
bool operator<(const edge &a,const edge &b)
{
return a.d<b.d;
}
int go(int p)
{
if(p==to[p])return to[p];
else return to[p]=go(to[p]);
}
int main()
{
n=read(),m=read();
for(int i=1;i<=n;i++)
{
to[i]=i;
}
for(int i=1;i<=m;i++)
{
ed[i].p1=read(),ed[i].p2=read(),ed[i].d=read();
}
sort(ed+1,ed+1+m);
for(int i=1;i<=m;i++)
{
int p1=ed[i].p1,p2=ed[i].p2;
if(go(p1)!=go(p2))
{
to[go(p1)]=to[go(p2)];
en++;
ans=max(ed[i].d,ans);
}
}
if(en==n-1)cout<<ans<<endl;
else cout<<-1<<endl;
return 0;
} //第一次 m、n 写反喜提 10 分
P1123 取数游戏
(秀一下我新学的 \(LaTeX\) 不过分吧 qwq)
これから、我が剣は汝と共にあり、汝の運命は我とともに存続する。
题意:
有 \(T\) 组数据;
每组数据给你一个 \(N \times M\) 的矩阵,选择若干个不相邻的元素使它们的和最大,求出这个最大的和。
\(1 \le N\)、\(M \le 6\),\(T \le 20\)。
不相邻是指 左、右、上、下、左上、左下、右上、右下 八个方向不相邻。
思路:
一看到 \(N\) 和 \(M\) 的取值范围,就知道这个题肯定是 爆搜!
那怎么搜索呢?
对于每一个数,我们有选择跟不选两种情况;
如果不选,可以继续搜索其他合法数。
如果选,那么我们就不能再选择它周围的数了,标记一下。
code:
#include<bits/stdc++.h>
using namespace std;
const int maxn=10;
const int maxm=10; //好习惯
int t;
int m,n;
int ans=0;
int mp[maxn][maxm];
int dx[]={0,1,-1,0,0,1,-1,1,-1};
int dy[]={0,-1,1,-1,1,1,-1,0,0}; //方向坐标
int mark[maxn][maxm];
void dfs(int x,int y,int sum)
{
if(y>m){
dfs(x+1,1,sum);
return;
}
if(x>n){
ans=max(ans,sum);
return;
}
//两种情况
dfs(x,y+1,sum); //不选的时候
if(!mark[x][y]) //选择的时候
{
for(int i=1;i<=8;i++)
{
int xx=x+dx[i],yy=y+dy[i];
mark[xx][yy]+=1; //将周围数的 mark +1
//这个地方不能用 vis 判断是否遍历过,因为它也可能是其他被选择的数的相邻数
}
dfs(x,y+1,sum+mp[x][y]); //sum 的值加上 选择的 这个数
for(int i=1;i<=8;i++)
{
int xx=x+dx[i],yy=y+dy[i]; //回溯
mark[xx][yy]-=1;
}
}
}
int main()
{
cin>>t;
while(t--)
{
ans=0;
memset(mp,0,sizeof(mp));
memset(mark,0,sizeof(mark)); //每次一定要清空
cin>>n>>m;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
cin>>mp[i][j];
}
}
dfs(1,1,0);
cout<<ans<<endl;
}
return 0;
}(参考题解过的TAT,我太菜了)
传送门:P1160 队列安排
为你,千千万万遍。
题目意思:
n 个同学排成一行,给你编号 2~n 的同学的位置信息 k、 p ,然后再给你 m 个删除操作,求最后的排列。
- 当 p = 0 时,表示将第 i 个同学插入第 k 个同学的左边;
- 当 p = 1 时,表示将第 i 个同学插入第 k 个同学的右边。
思路:
链表(以为自己不会链表,结果就是个链前存图 qwq)
不同的就是要同时存 左边和右边,每两个学生之间有且只有一条路径
code:
#include<bits/stdc++.h>
using namespace std;
int n,m;
bool vis[100010];
struct people{
int left,right;
int vis;
}pe[100010];
void add_edge(int u,int v,int p)
{
if(p==1){//u = k
//i = v
pe[u].left=v;
pe[u].right=pe[v].right;
pe[pe[v].right].left=u;
pe[v].right=u;
}
else{
pe[u].right=v;
pe[u].left=pe[v].left;
pe[pe[v].left].right=u;
pe[v].left=u;
}
}
int main()
{
cin>>n;
pe[0].left=0,pe[0].right=0; //这个地方要从 0 开始
add_edge(1,0,1);
for(int i=2;i<=n;i++)
{
int k,p;
cin>>k>>p;
add_edge(i,k,p);
}
cin>>m;
for(int i=1;i<=m;i++)
{
int k;
cin>>k;
pe[k].vis=1;
}
for(int i=pe[0].right;i;i=pe[i].right)
{
if(pe[i].vis==0)cout<<i<<" ";
}
return 0;
}
P1510 精卫填海
背包 DP 复习
- 01 背包
顾名思义,一个东西只有选和不选两种选择。
求体积一定的包里能放的最大质量。
for(int i=1;i<=n;i++)
{
for(int j=m;j>=w[i];j--)//w[i]表示物品 i 的体积
{
f[j]=max(f[j],f[j-w[i]]+v[i]);//v[i]表示物品 i 的质量
//f[j]表示 背包容量为 j 时最多能放的质量
}
}
为什么要逆序
首先,通过上一个问题,我们确认了我们目前一维的dp数组,保存的是确认过的最新一层的数据,即上一层的数据。
当我们计算当前层时,对于二维时的状态转移方程有
dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
可以看到,dp[i - 1][j - v[i]] + w[i] 使用的上一层的原始数据(dp[i - 1]),而我们使用一维的状态转移方程时有
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
当我们从小到大更新是, 因为j - v[i] 是严格小于j 的,所以我们可以举个例子 dp[3] = max(dp[3], dp[2] + 1); 因为我们是从小到大更新的,所以当更新到dp[3]的时候,dp[2]已经更新过了,已经不是上一层的dp[2]。
而当我们逆序更新时有,举例 dp[8] = max(dp[8], dp[6] + 2)当更新dp[8]时,dp[6]还没有被更新,还是上一层的数据,这样才能保证没有读入脏数据。
以上来自 AcWing
- 完全背包
与 01 背包的区别就是:同一个物品可以被放多次
for(int i=1;i<=n;i++)
{
for(int j=w[i];j<=m;j++)//这里的枚举顺序要改变一下
{
f[j]=max(f[j],f[j-w[i]]+v[i]);
//f[j]表示总体积是 j 时最大价值是多少
}
}
枚举顺序改变的原因:
每次再使用 f[j] 的时候状态就是已经更新过的
- 多重背包
在完全背包的基础上,每个物品是有限定数量的。
(1) 可以把每种物品看成 cnt 个物品,只不过他们的质量体积都相同,这样就能转化为 01 背包问题。
(2) 每个物品有 不选、选一个、选两个、………… 选 cnt 个,加一重循环就能实现。
好像两个运行时间差不多?
代码(方法 2 )
for(int i=1;i<=n;i++)
{
cin>>w[i]>>v[i]>>cnt[i];
}
for(int i=1;i<=n;i++)
{
for(int j=w[i];j<=m;j++)
{
for(int k=0;k<=cnt[i];k++)
{
f[j]=max(f[j],f[j-k*w[i]]+k*v[i]);
}
}
}
回到这道题
明显的 01 背包
求的是剩下的最大体力,就是求消耗的最小体力。将体力看做背包 将石子看做物品。
\(f[ j ]=max( f[ j ],f[ j-w[ i ] ]+v[ i ])\)
求一边后再从小到大搜 j ,找到一个石子 >= 要求的最小解,再输出 总体力 - 最小解。
code:
#include<bits/stdc++.h>
using namespace std;
int V,n,c;
int w[10010];
int v[10010];
int f[100010];
int main()
{
cin>>V>>n>>c;
for(int i=1;i<=n;i++)
{
cin>>w[i]>>v[i];
}
for(int i=1;i<=n;i++)
{
for(int j=c;j>=v[i];j--)
{
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
}
for(int j=0;j<=c;j++)
{
if(f[j]>=V){
cout<<c-j<<endl;
return 0;
}
}
cout<<"Impossible"<<endl;
return 0;
}
传送门:P10995 【MX-J3-T2】Substring
本来是一道黄题,结果它橙了。。。(流出做不出橙题的泪)
题目意思:
给你一个数列,数列中每个数只出现了一次,求字典序为第 k 个的子串的左右端点。
思路
贪心、前缀和、二分。
由于查询的是子串,那么串一定连续。这时候子串排序的结果一定是按照 \(1\) 开头,\(2\) 开头,...,\(n\) 开头这样的顺序排的。
设这个数字为 \(a_i\),则 \(a_i\) 开头的子串数量就是 \(n-i+1\)。
知道了这一点后,我们就可以先记录每个数字的位置 \(id_{a_i}\),再把数字排序,计算出每个数开头的子串数量,最后计算前缀和。
每次查询的时候找第一个前缀和 \(\ge k≥k\) 的位置 \(p\),说明第 \(k\) 个子串肯定在 \(a_p\) 开头的范围内。
现在我们知道左端点为 \(id_p\),接着计算右端点。
此时 \(sum_p\) 为加上 \(a_p\) 开头的所有数之后的前缀和,所以 \(k-sum_{p-1}\)即为要往后延长的数量。
但是注意这样查的范围是右界 \(+1\),所以最后还要 \(−1\),即右端点为 \(id_p+k-sum_{p-1}-1\)。
子串数最大为 \(\Large\frac{n(n+1)}{2}\),\(1\le n\le 3\times 10^5\),
记得开 long long。
- 以上转载于这位大佬的题解
代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,q;
struct number{
int a,pos;
}a[300100];
int rr[300100];
int cnt[300100];
bool cmp(const number &a,const number &b)
{
return a.a<b.a;
}
bool check(int m,int p)
{
if(cnt[m]>=p)return true;
else return false;
}
signed main()
{
cin>>n>>q;
for(int i=1;i<=n;i++)
{
cin>>a[i].a;
a[i].pos=i;
rr[a[i].a]=i;
}
sort(a+1,a+1+n,cmp);
for(int i=1;i<=n;i++)
{
cnt[i]=n-a[i].pos+1+cnt[i-1];
}
for(int i=1;i<=q;i++)
{
int p;
cin>>p;
int l=1,r=n;
while(l<r){
int mid=l+r>>1;
if(check(mid,p))r=mid;
else l=mid+1;
}
cout<<a[l].pos<<" "<<a[l].pos-1+p-cnt[l-1];
cout<<endl;
}
return 0;
}
P3398 仓鼠找 sugar
题目意思:
给你一棵树,有 \(q\) 次询问
给你两对 \(u\) 和 \(v\),求它们的最短路径是否有重合的点。
对于每个询问,如果有公共点,输出大写字母 Y;否则输出 N
思路
树上最短路径很容易想到是 \(u\to lca(u,v)\to v\)
观察下面的图:
假设我们此时要查询的两组 \(u\),\(v\) 分别为 \(2\),\(7\) 和 \(4\),\(8\)
\(lca(2,7)\) 的值为 \(1\),路径 1 为 \(2\to1\to4\to5\to7\)
\(lca(4,8)\) 的值为 4,路径 2 为 \(4\to5\to8\)
很容易看出如果两条路径有重点,那么一定有一条路径的lca在另一条路径上,并且这个lca是深度比较大的那一个
并且当 x 为 u 到 v 的路径上的点时:
- \(depth_x>=depth_{LCA(u,v)}\)
- \(LCA(u,x)=x\) 或 \(LCA(v,x)=x;\)
粗略的证明:
当\(depth_x<depth_{lca(u,v)}\) 时,说明一定不可能在路径上。
如果上一个条件满足,当 \(lca(u,x)=x\) 或 \(lca(v,x)=x\) 时,说明从 \(u\) 或 \(v\) 走到 \(x\) 不需要有拐点,并且前面已知从 \(u\) 走到 \(v\) 的拐点为 \(lca(u,v)\),\(x\)的深度等于 lca 或比 lca 大,说明 \(u\) 到 lca 或 \(v\) 到 lca 会经过 \(x\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int maxn=100010;
const int maxm=100100;
int n,q;
int en;
int fir[maxn];
int f[maxn][25];
struct edge{
int v,next;
}ed[maxm*2];
void add_edge(int u,int v)
{
en++;
ed[en].v=v;
ed[en].next=fir[u];
fir[u]=en;
}
int depth[maxn];
void dfs(int now,int fa)
{
depth[now]=depth[fa]+1;
f[now][0]=fa;
for(int i=1;i<=20;i++)
{
f[now][i]=f[f[now][i-1]][i-1];
}
for(int i=fir[now];i;i=ed[i].next)
{
if(ed[i].v!=fa) dfs(ed[i].v,now);
}
}
int get_lca(int a,int b)
{
if(depth[a]<depth[b]) swap(a,b);
for(int i=20;i>=0;i--)
{
if(depth[f[a][i]]>=depth[b])
a=f[a][i];
}
if(a==b) return a;
for(int i=20;i>=0;i--)
{
if(f[a][i]!=f[b][i])
a=f[a][i],b=f[b][i];
}
return f[a][0];
}
int main()
{
cin>>n>>q;
for(int i=1;i<n;i++)
{
int u,v;
cin>>u>>v;
add_edge(u,v);
add_edge(v,u);
}
dfs(1,0);
for(int i=1;i<=q;i++)
{
int a,b,c,d;
cin>>a>>b>>c>>d;
int lca1=get_lca(a,b);
int lca2=get_lca(c,d);
if(depth[lca1]>=depth[lca2]) //查找c到d的路径上有没有lca1
{
if(get_lca(c,lca1)==lca1||get_lca(d,lca1)==lca1)cout<<"Y"<<endl;
else cout<<"N"<<endl;
}
else{
if(get_lca(a,lca2)==lca2||get_lca(b,lca2)==lca2)cout<<"Y"<<endl;
else cout<<"N"<<endl;
}
}
return 0;
}
P5542 [USACO19FEB] Painting The Barn S
思路:
二维差分
定义数组差分数组 \(cf_{i,j}\)
很容易想到差分数组的求法:\(cf_{i,j}=a_{i,j}-cf_{i-1,j}-cf_{i,j-1}+cf_{i-1,j-1}\);
最后统计数据的方法就是:\(cf_{i,j}+=cf_{i-1,j}+cf_{i,j-1}-cf_{i-1,j-1}\)
将点 x1,y1 到 x2,y2 更改的方法:
cf[x1][y1] += c;
cf[x1][y2+1] -=c;
cf[x2+1][y1] -=c;
cf[x2+1][y2+1] += c;
注意:
1。目给的是二维坐标系的左下角和右上角,实际上就是我们平常使用差分或前缀和时,用行列表示的左上角和右下角(相当于把坐标系转一下),所以直接拿来用就好了,不需要进行转化
2。题目告知的是点的坐标,而差分和前缀和都是在格子上实现的,所以要把点转化成格子,即左上角的点横坐标和纵坐标都+1(不会的话可以画个图模拟下)
代码:
#include<bits/stdc++.h>
using namespace std;
int n,k;
int cf[10010][10010];
int ans=0;
int main()
{
cin>>n>>k;
for(int i=1;i<=n;i++)
{
int a,b,c,d;
cin>>a>>b>>c>>d;
a++,b++;
cf[a][b]++;
cf[c+1][d+1]++;
cf[a][d+1]--;
cf[c+1][b]--;
}
for(int i=1;i<=1000;i++)
{
for(int j=1;j<=1000;j++)
{
cf[i][j]+=cf[i-1][j]+cf[i][j-1]-cf[i-1][j-1];
if(cf[i][j]==k) ans++; }
}
cout<<ans<<endl;
return 0;
}