ACM题集:https://blog.csdn.net/weixin_39778570/article/details/83187443
题目链接:https://codeforces.com/contest/1061
官方题解:https://codeforces.com/blog/entry/63384
题意:1,2,3,4,…n每个数都有无限个,问数k最少能由1~n中几个数组成
#include
#define ll long long
using namespace std;
ll n,s;
int main(){
scanf("%lld%lld",&n,&s);
ll all=0;
for(ll i=n; i>=1; i--){
if(s>=i){
all += s/i;
s%=i;
}
if(s<=0)break;
}
cout<<all;
return 0;
}
题意:给定一个m列的柱状图,如m=3,图为4,5,6,表示连续的3个柱高(由方格组成)为4,5,6
这个柱状图在往上投影为3,往左投影为6
问,最多能去掉几个方格使得往上投影和往左投影与一开始一样。
解法:
把题目去掉格子,装换成填充格子(填充的格子是需要投影的),我们可以知道往上投影每一列放一个就能满足,但是往左投影需要所有列填放的高度(同一高度的不算)加起来与最高一列相同
m = 4; 序列5, 2, 2, 2
例如填充一个高度为5的可以这么填充
1 0 0 0
1 0 0 0
1 0 0 0
0 1 0 0
0 0 1 1
先排序好,从高到低进行填充,最后答案=方块数-填充数
枚举到哪一个时,就表示比当前高的全部填充过了
a[0]初始化为无穷大
枚举第i个时
当a[i] > a[i+1] 的时候,填充 a[i]-a[i+1]个
当a[i] == a[i+1] 的时候, 第i列填充1个,往后和他相等的都变为a[i]-1
#include
#define ll long long
#define fo(i,j,n) for(register int i=j; i<=n; i++)
using namespace std;
ll n,m,a[100005],mx2,mx,mi=1e18,sum;
int main(){
cin>>n>>m;
fo(i,1,n){
scanf("%I64d",&a[i]);
sum += a[i];
}
sort(a+1,a+1+n);
reverse(a+1,a+1+n);
if(n==1)puts("0");
else{
ll ans = 0;
a[0]=1e9+5;
fo(i,1,n){
if(a[i-1]<a[i])a[i]=a[i-1]; // 与前面高度持平
if(a[i]<=a[i+1]) { // 超过部分已经在前面通过若干个1填充过了
ans ++;
a[i]--; // 后面的 相同高度都变为a[i]-1
}
else ans += max(1ll,a[i]-a[i+1]);
}
cout<<sum-ans;
}
return 0;
}
题目:给定一个序列A,使用序列A可以构造序列B。序列B要满足这样的条件:
序列B: b 1 , b 2 , b 3 , b 4.. b n b1,b2,b3,b4..bn b1,b2,b3,b4..bn,且 1 ∣ b 1 , 2 ∣ b 2 , 3 ∣ b 3... n ∣ b n 1|b1,2|b2,3|b3...n|bn 1∣b1,2∣b2,3∣b3...n∣bn,也就是说bk要能被k整除
问最多有多少个B数组,B数组不同当有一个bk不同,或者B数组的长度不同。
解法:
d p [ i ] [ j ] dp[i][j] dp[i][j] 表示第i个数 a [ i ] a[i] a[i],数组B长度为j是的方案数
当 j j j是 a [ i ] a[i] a[i]的约数时,可以选或者不选 a [ i ] a[i] a[i]作为第 j j j个数
d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + d p [ i − 1 ] [ j − 1 ] dp[i][j] = dp[i-1][j] + dp[i-1][j-1] dp[i][j]=dp[i−1][j]+dp[i−1][j−1]
否则
d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i-1][j] dp[i][j]=dp[i−1][j]
a n s = d p [ n ] [ i ] ans = dp[n][i] ans=dp[n][i], i ∈ [ 1 , m a x ( a [ i ] ) ] i ∈[1,max(a[i])] i∈[1,max(a[i])]
每个 a [ i ] a[i] a[i]在阶段 i i i只能使用一次,并且从阶段 i − 1 i-1 i−1递推过来
可以通过反向递推 j j j 减低为1维
p [ i ] p[i] p[i]是 a [ i ] a[i] a[i]的因子,也就是说 p [ i ] p[i] p[i]是 a [ i ] a[i] a[i]所能填充的位置
从大到小枚举 p p p
d p [ p [ k ] ] = d p [ p [ k ] ] + d p [ p [ k ] − 1 ] dp[p[k]] = dp[p[k]] + dp[p[k]-1] dp[p[k]]=dp[p[k]]+dp[p[k]−1]
表示选或者不选 a [ i ] a[i] a[i]的因子 p [ k ] p[k] p[k]作为第 p [ k ] p[k] p[k]个数
#include
#define ll long long
#define fo(i,j,n) for(register int i=j; i<=n; ++i)
using namespace std;
const int mod = 1e9+7;
int n, a[100005],p1[100005],p2[100005];
ll f[1000005]; // f[n] = f[n] + f[n-1]
void solve(){
f[0]=1;
fo(i,1,n){ // 第i个数
// 分解因子,这些因子决定了a[i]能放到第几个数
int t = sqrt(a[i]+0.5);
int j,k=1,k2=1;
for(j=1; j<=t; j++){
if(j*j==a[i]){
k2=k;
p1[k++]=j;
break;
}
if(a[i]%j==0){
p1[k]=j;
p2[k++]=a[i]/j;
}
}
if(j*j!=a[i])k2=k;
for(j=1; j<k2; j++) // 从大到小枚举,保证当前一轮,a[i]只使用一次
f[p2[j]] = (f[p2[j]]+f[p2[j]-1]) % mod;
for(j=k-1; j>=1; j--)
f[p1[j]] = (f[p1[j]]+f[p1[j]-1]) % mod;
}
ll ans = 0;
fo(i,1,1000000) ans = (ans+f[i])%mod;
cout<<ans;
}
int main(){
cin>>n;
fo(i,1,n)scanf("%d",&a[i]);
solve();
return 0;
}
题意:有n档节目,节目的播放时间为 [ L 1 , R 1 ] , [ L 2 , R 2 ] , [ L 3 , R 3 ] , . . . [ L n , R n ] [L1,R1],[L2,R2],[L3,R3],...[Ln,Rn] [L1,R1],[L2,R2],[L3,R3],...[Ln,Rn]
现在需要把所有的节目都播放完,租一台电视机的费用为x元(作为第一时刻的播放价格),往后每租单位时间的价格为y,也就是说租借 [ L k , R k ] [Lk,Rk] [Lk,Rk]这段时间一台电视机的花费为 x + ( R k − L k ) ∗ y x+(Rk-Lk)*y x+(Rk−Lk)∗y,问:最少需要花费多少钱看完所有节目
5 4 3(n,x,y)
1 2[L,r]
4 10
2 4
10 11
5 9
Show [1,2] on the first TV,
Show [4,10] on the second TV,
Shows [2,4],[5,9],[10,11] on the third TV.
This way the cost for the first TV is 4+3⋅(2−1)=7, for the second is 4+3⋅(10−4)=22 and for the third is 4+3⋅(11−2)=31, which gives 60 int total.
解法:对于每个区间 [ L k , R k ] [Lk,Rk] [Lk,Rk],我们需要考虑新开一个区间的花费少,还是与某个区间 [ L i , R i ] [Li,Ri] [Li,Ri]合并的花费少.
也就是比较 x 和 ( L k − R i ) ∗ y x和(Lk-Ri)*y x和(Lk−Ri)∗y的大小,Ri为当前已经有的区间中最大的R,毕竟浪费的时间越少,花费的钱就越少
法一:93ms
先把区间按左端点排序
使用一个多重集合(可以看做是一个有序的vector),二分查找小于L的最大值
事先插入所有的右端点,就不用在线更新右端点了
因为查找的顺序是按区间的左端点排序好的
所以不会出现查找到一个还未出现的右端点,
因为还为出现的右端点一定比当前左端点大,所以无法查找到
#include
using namespace std;
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); //不能和scanf一起用
#define endl "\n"
#define int long long
const int N=1e5+5;
const int MOD=1e9+7;
int n, x, y, ans=0;
int cost[N];
pair<int, int> a[N];
multiset<pair<int, int> > s;
int32_t main()
{
cin>>n>>x>>y;
for(int i=1;i<=n;i++)
{
scanf("%I64d%I64d",&a[i].first,&a[i].second);
s.insert({a[i].second, a[i].first}); // 把所有区间都加进去了,后面就不用加了,其实维护的主要还是右端点
}
sort(a+1, a+n+1);
for(int i=1;i<=n;i++)
{
cost[i] = (x + y * (a[i].second - a[i].first)); // 结构体指针要用->访问
if(!s.size() || s.begin() -> first >= a[i].first) // 最小的左端点>=右端点,二分查找找不到结果
continue;
auto it = s.lower_bound({a[i].first, 0}); // 找到>=a[i].first第一个右端点
int pR = (--it) -> first; // 小于a[i]左端点的最大右端点
if (y * (a[i].second - pR) >= cost[i]) // 连接不如新开
continue;
cost[i] = y * (a[i].second - pR); // 连接
s.erase(it);
}
for(int i=1;i<=n;i++)
{
ans+=cost[i];
ans%=MOD;
}
ans%=MOD;
cout<<ans;
return 0;
}
法二:93ms
和法一一样,只不过每次是在线插入右端点的
#include
#define ll long long
#define fo(i,j,n) for(register int i=j; i<=n; ++i)
using namespace std;
const int maxn = 1e5+5;
const int mod = 1e9+7;
ll n,x,y;
pair<ll,ll>a[maxn];
multiset<ll> R;
void solve(){
sort(a+1,a+1+n);
ll ans = 0,l,r;
fo(i,1,n){
l = a[i].first,r=a[i].second;
if(R.empty()|| *(R.begin())>=l){ // 无法二分到答案
ans += x+(r-l)*y;
if(ans>=mod)ans%=mod;
R.insert(r); // 在线插入
continue;
}else{
auto it = R.lower_bound(l); // >=的第一个值
int PR = *(--it); // 解引用 ,小于L的最大的值
if((l-PR)*y<x){
ans += (r-PR)*y;
if(ans>=mod)ans%=mod;
R.insert(r);
R.erase(it); // 合并后要删除旧区间的右端点
}else{
ans += x+(r-l)*y;
if(ans>=mod)ans%=mod;
R.insert(r); // 新开区间
}
}
}
cout<<ans;
}
int main(){
cin>>n>>x>>y;
fo(i,1,n)scanf("%I64d%I64d",&a[i].first,&a[i].second);
solve();
return 0;
}
法三:vector做法 780ms
与法一一样,但是换了中数据结构
#include
#define ll long long
#define fo(i,j,n) for(register int i=j; i<=n; i++)
using namespace std;
const int MOD = 1e9+7;
int n,x,y;
vector<pair<ll,ll>> a;
vector<ll> R; // 存放所有右端点
ll cost[100005];
void solve(){
sort(a.begin(),a.end());
sort(R.begin(),R.end());
fo(i,0,n-1){
cost[i] = (x+y*(a[i].second-a[i].first));
if(!R.size() || R[0]>=a[i].first)continue;
auto it = lower_bound(R.begin(),R.end(),a[i].first);
ll pR = R[(--it)-R.begin()];
if(y*(a[i].second-pR)>=cost[i])continue;
cost[i] = y*(a[i].second-pR);
R.erase(it); // 很耗时...
}
ll ans=0;
fo(i,0,n-1){
ans+=cost[i];
ans%=MOD;
}
ans%=MOD;
cout<<ans;
}
int main(){
cin>>n>>x>>y;
ll l,r;
fo(i,1,n){
int l,r;
scanf("%I64d%I64d",&l,&r);
a.push_back(make_pair(l,r));
R.push_back(r);
}
solve();
return 0;
}
法四:单调栈+优先列队做法 77ms
优先列队q,存放所有左端点
单调栈s,存放所有比L小的R,并且有序。
q是小根堆
维护s的方法:
首先对区间的左端点进行升序排序
对于每个[L,R],从小根堆里剔除比L小的R依次加到栈s中,这样每次加入栈s的R都是有序的
又由于L也是有序的,所以整个s也是有序的
每次剔除完R后,在线插入当前R
#include
#define ll long long
#define fo(i,j,n) for(register int i=j; i<=n; i++)
using namespace std;
const int maxn = 1e5+5;
const int mod = 1e9+7;
ll n,x,y;
stack<ll> s; // 右区间单调栈,储存可能更新的答案
priority_queue<ll,vector<ll>, greater<ll> > q; // 右小根堆
pair<ll,ll> a[maxn]; // 左区间上升
void solve(){
sort(a+1,a+1+n); // 一定要排序
ll ans = 0,l,r;
fo(i,1,n){
l=a[i].first, r=a[i].second;
while(!q.empty() && q.top()<l){
s.push(q.top());q.pop(); // 假设单前为L1,那么压入栈的数小于L1,下一次压入栈的数介于 [L1,L2)之间
} // 整个栈单调递增
if(s.empty()){
ans += x+(r-l)*y;
if(ans>=mod)ans%=mod;
}else{ // 小于l的最大的r
if((l-s.top())*y<x){
ans += (r-s.top())*y;
if(ans>=mod)ans%=mod;
s.pop();
}else{
ans += x+(r-l)*y;
if(ans>=mod)ans%=mod;
}
}
q.push(r);
}
cout<<ans;
}
int main(){
cin>>n>>x>>y;
fo(i,1,n){
scanf("%I64d%I64d",&a[i].first,&a[i].second);
}
solve();
return 0;
}
题意:有一个完美k叉树,且叶子节点高度都一样
可以向系统询问 ? a b c, 表示问:b是否在ac路径中间
通过不大于60n次询问找出根节点
节点和序号的不一致的
解法:
随机生成两个节点 V 1 , V 2 ( V 1 ! = V 2 , 1 < = V 1 , V 2 < = n ) V1,V2(V1!=V2,1<=V1,V2<=n) V1,V2(V1!=V2,1<=V1,V2<=n)
先找出两个叶子节点,两个叶子节点中间的点有2h-1个(h为高度)
然后找到一个点到和其中一个叶子节点 ,中间的点数有h-1个的点,那么该点就是跟节点
#include
#define ll long long
#define fo(i,j,n) for(register int i=j; i<=n; ++i)
using namespace std;
int n,k;
vector<int> ver;
bool check(int v1 ,int i, int v2){
printf("? %d %d %d\n",v1,i,v2);
string s;
cin>>s; // 会有空行,使用cin
return s[0]=='Y';
}
int main(){
cin>>n>>k;
int h=0,a=1,leaf=1;
while(a<n){
leaf *= k;
a += leaf;
h++; // 计算树高
}
int v1,v2; // 左右两个叶子节点
// 随机计算左右两个叶子节点
while(true){
ver.clear();
v1 = rand()%n + 1; // 1~n
v2 = rand()%(n-1) + 1; // 1~n-1
if(v1==v2)v2++;
fo(i,1,n){
if(i!=v1&&i!=v2&&check(v1,i,v2))ver.push_back(i);
}
if(ver.size() == 2*h-1)break; // 找到两个叶子节点
}
for(int p:ver){
int num = 0; // 记录p,v1之间的点数
for(int q:ver){
if(p!=q && check(p,q,v1))num++;
}
if(num==h-1){ // 找到root了
printf("! %d\n",p);
break;
}
}
return 0;
}