[18.8.20]校内NOIP模拟赛
- 所有题目均可以在 https://nanti.jisuanke.com/?kw=The%202018%20ACM-ICPC%20Chinese%20Collegiate%20Programming%20Contest找到
T1 旋转多边形
题意
给你一个凸多边形,和多边形内一点$P$,求其滚动一圈,点$P$的轨迹长。
$0\leq n\leq50$
题解
**题。对于每个顶点求其与点$P$的距离和两条边的夹角即可。
代码
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<queue>
#include<cmath>
using namespace std;
template<typename T>
inline void read(T& s)
{
s=0;int f=1;char c=getchar();
while(c<'0' || c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0' && c<='9'){s=s*10+c-'0';c=getchar();}
s*=f;
}
const double pi = acos(-1);
template <typename T>
T rabs(T x)
{
return x>0?x:-x;
}
struct node
{
int x,y;
node(int a=0,int b=0)
{
x=a;
y=b;
}
void readp()
{
read(x);
read(y);
}
double the()
{
return atan2(double(x),double(y));
}
double dis()
{
return sqrt((double)(x*x+y*y));
}
}s[100],xx;
node operator - (node a,node b)
{
return node(a.x-b.x,a.y-b.y);
}
int n;
int main()
{
read(n);
for(int i=0;i<n;i++)
s[i].readp();
xx.readp();
double ans=0;
for(int i=0;i<n;i++)
{
int ne=(i+1)%n;
int la=(i+n-1)%n;
double th=(s[ne]-s[i]).the()-(s[la]-s[i]).the();
while(th<0) th+=pi;
while(th>pi) th-=pi;
th=pi-th;
double r=(xx-s[i]).dis();
ans+=th*r;
}
printf("%.3lf\n",ans);
return 0;
}
T2 冒泡排序
题意
求冒泡排序外层循环执行$k$次后的序列。
$2\leq n \leq 10^5$。
题解
首先观察发现 冒泡排序每进行一次,每个数字最多向后移动一次,但向前移次数没有限制。
也就是说,最终序列的第$1$项一定是原序列的第$1$项到第$k+1$项里面其中一个。
显而易见,第$1$项一定是这里面最小的一个(如果存在一个比它更小的,那么$k$次操作后它一定在这个数字的前面,所以最小的一个一定是第$1$项)。
同理,第$2$项一定是剩余数字加上第$k+2$项内最小的一个(因为向前移动次数没有限制,所以需要继承上次的集合)。
因此,最终只需要维护一个大小始终为$k+1$的堆,然后每次取最小值即可。
代码
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<queue>
using namespace std;
template<typename T>
inline void read(T& s)
{
s=0;int f=1;char c=getchar();
while(c<'0' || c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0' && c<='9'){s=s*10+c-'0';c=getchar();}
s*=f;
}
priority_queue<int>q;
int n,k;
int s[100005];
int ans[100005];
int tot=0;
int main()
{
read(n);
read(k);
for(int i=0;i<n;i++)
read(s[i]);
if(k>=n)
{
sort(s,s+n);
for(int i=0;i<n;i++)
printf("%d ",s[i]);
printf("\n");
return 0;
}
for(int i=0;i<k;i++)
q.push(-s[i]);
for(int i=k;i<n;i++)
{
q.push(-s[i]);
ans[tot++]=-q.top();
q.pop();
}
while(!q.empty())
{
ans[tot++]=-q.top();
q.pop();
}
for(int i=0;i<n;i++)
printf("%d ",ans[i]);
printf("\n");
return 0;
}
T3 修建工厂
题意
给你一棵树,要求选$k$个叶子,使两两之间距离和最小。
$3\leq n \leq 10^5,0\leq k \leq 100$
题解
这题一眼树上背包。$dp_{i,j}$表示当前转移到了以$i$为根的子树,选了$j$个叶子的最小代价。
转移方法显而易见。
时间复杂度$O(nk)$
以下是(不够严谨的)证明。
显而易见,将选择叶子改为选择节点后,复杂度不变。
先考虑此问题的弱化版,限制不为$k$而是$n$。
对于$x$来讲,每一次和子节点$v$的子树合并,需要枚举$v$的子树大小和之前已经合并过的$x$的子树(以下称之为左侧子树)的大小。
显然,我们可以视为枚举左侧子树的每一个点以及$v$的子树的每一个点。两者的计算次数是相等的。
那么我们可以发现,任意两个点当且仅当在其最近公共祖先处被合并。
所以总复杂度为$O(n^2)$。
来看原问题。
如果需要合并后的子树大小没有超过$k$,和上面问题等价。
如果合并后的子树大小超过了$k$,那么选择的个数上限取$k$。
我们可以认为,个数上限为$k$枚举个数,与子树大小为$k$枚举子节点是等价的,显然这部分的上界是$O(nk)$。
所以当合并后子树超过$k$时,超过的部分会被删掉,设多余的部分为$x$。每次合并的计算次数大约是$kx$(不严谨,具体次数还和儿子的具体大小有关,均摊似乎是这个?),这部分是$O(nk)$的。
所以此问题解法复杂度为$O(nk)$。
代码
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<queue>
using namespace std;
template<typename T>
inline void read(T& s)
{
s=0;int f=1;char c=getchar();
while(c<'0' || c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0' && c<='9'){s=s*10+c-'0';c=getchar();}
s*=f;
}
int box[100005],las[200005],edv[200005],edw[200005],cnt=0;
void adde(int u,int v,int w)
{
las[++cnt]=box[u];
box[u]=cnt;
edv[cnt]=v;
edw[cnt]=w;
}
int n,k;
int rot;
int dd[100005];
long long dp[100005][105];
int yzc[100005];
void dfs(int now,int fa)
{
if(dd[now]==1)
{
dp[now][0]=dp[now][1]=0;
yzc[now]=1;
return;
}
dp[now][0]=0;
for(int i=box[now];i;i=las[i])
if(edv[i]!=fa)
{
int v=edv[i];
int w=edw[i];
dfs(v,now);
for(int i=min(yzc[now],k);i>=0;i--)
for(int j=1;j<=yzc[v] && i+j<=k;j++)
dp[now][i+j]=min(dp[now][i+j],dp[now][i]+dp[v][j]+1ll*j*(k-j)*w);
yzc[now]+=yzc[v];
}
}
int main()
{
read(n);
read(k);
int a,b,c;
memset(dp,23,sizeof(dp));
for(int i=1;i<n;i++)
{
read(a);
read(b);
read(c);
dd[a]++;
dd[b]++;
adde(a,b,c);
adde(b,a,c);
}
for(int i=1;i<=n;i++)
if(dd[i]!=1)
{
rot=i;
break;
}
dfs(rot,0);
printf("%lld\n",dp[rot][k]);
return 0;
}