1. 程式人生 > 實用技巧 >UVA11610 【Reverse Prime】

UVA11610 【Reverse Prime】

本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本


思考

這一題,它的第一個要求是找出所有 \(7\) 位反向質數及其質因數的個數。

我們應該需要質數篩篩選1~\(10^{7}\)的所有數,這裡就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 \(7\) 位,而將它反過來後的數字卻是 \(6\) 位數,這就說明了最後一位數肯定是0,這樣,我們就可以只篩到\(10^{6}\),也就是說,篩所有反向數,如果是質數,反向過來,末尾填 \(0\) 就是了。這樣,我們就減少了時間複雜度,這一部分函式的程式碼如下:

#define N 1e6+10
void Euler_prime()   //尤拉篩
{

	memset(is_prime,true,sizeof(is_prime));   //先全部標記為是質數  
	is_prime[1]=false;	//特判一下1不是質數
	for(int i=2;i<N;i++)	//列舉2~N的每個數
	{
		if(is_prime[i]) prime[++tot]=i;	//如果是質數
		for(int j=1;j<=tot;j++)	
		{
			if(prime[j]*i>N)
				break;
			is_prime[prime[j]*i]=false;
			if(i%prime[j]==0)   
				break;
		}
	}
}
int solve(int num)  //實現了num數的反向
{
    int len=0,ret=0,bit[20];
    while(num)  
    {
        bit[len++]=num%10;
        num/=10;
    }
    for(int i=0;i<len;i++)
    {
        ret=ret*10+bit[i];
    }       //本函式到這裡實現了res=num的反向數
    while(ret<100000) ret*=10;  //因為求出來的數不一定是6位數,所以要加上這行程式碼,末尾填0
    return ret;
}

這裡求完了,接著就要算出它的質因數的個數,這個應該是非常簡單的了。但是還要注意,因為我們是篩選的1~\(10^{6}\)的質數,再反過來在末尾填 \(0\) 的,順序都被打亂了,所以我們要將所有反向質數從小到大排序。這裡為了離散化,使用了 \(STL\) 庫中的 \(map\),不懂的同學可以自行百度一下。初始化的部分如下:

map<int,int>m;  //為了能夠離散化,我使用了map
void init() //初始化函式
{
    Euler_prime();   //先篩了吧……
    for(int i=1;i<=tot;i++)
        a[i]=solve(prime[i]);   //a[i]表示第prime[i]個數的反向質數
    sort(a+1,a+1+tot);  //此題需要從小到大排序,才能在後面的二分中查詢
    for(int i=1;i<=tot;i++)
        m[a[i]]=i;  //將其離散化
/*以下函式部分實現了求i的質因數*/
    for(int i=1;i<=tot;i++) 
    {
        fac[i]=2;	//fac[i]用來貯存第i個反向質數的質因數的數量,初值一定要賦2
        int tmp=a[i];
        for(int j=1;j<=tot&&prime[j]*prime[j]<=tmp;j++) 
        {
            while(tmp%prime[j]==0)
            {
                tmp/=prime[j];
                fac[i]++;
            }
        }
        if(tmp>1) fac[i]++; //在特殊情況下,tmp未除盡,質因數的數量要再加一個
    }
}

現在,再來看看題目給我們的 \(2\) 個操作,一個是刪除,一個是查詢。如果我們用普通的模擬來解決的話,肯定是會超時的。這裡,我們就會想到一種特殊的資料結構——樹狀陣列,也就是此題的關鍵。由於題目的特殊性,這裡,我們必須要定義兩個樹狀陣列,一個樹狀陣列 \(c1\) 記錄當前 \(i\) 節點之前有多少個數,而另一個 \(c2\) 記錄了從第一個位置到當前位置反向質數的質因數的個數的總和。在查詢時,我們還要注意一下,要找出第 \(0\)\(i\) 的所有的反向質數的質因數的數量的累計總和,我們需要找出它的位置,因此可以用二分來解決這個問題,這時,問題就變得很簡單。

樹狀陣列的程式碼如下:

/**********以下是樹狀陣列基本函式**********/
int lowbit(int k)   //神奇的lowbit函式
{
    return k&-k;
}
void add1(int x,int v)  //
{
    for(int i=x;i<N;i+=lowbit(i))
        c1[i]+=v;
}
void add2(int x,int v)  //
{
    for(int i=x;i<N;i+=lowbit(i))
        c2[i]+=v;
}
ll getsum1(int x)   //
{
    ll ans=0;
    for(int i=x;i>0;i-=lowbit(i))
        ans+=c1[i];
    return ans;
}
ll getsum2(int x)   //
{
    ll ans=0;
    for(int i=x;i>0;i-=lowbit(i))   
        ans+=c2[i];
    return ans;
}
/**********以上是樹狀陣列基本函式**********/
int main()
{
    init(); //初始化......
	char op[2]; //因為一個字元的輸入總是有問題,所以就定義字串啦
    int k;
    for(int i=1;i<=tot;i++)
    {
        add1(i,1);  //存在則壓入數字1
        add2(i,fac[i]); //當前位置有多少個反向質數
    }
    while(scanf("%s%d",op,&k)!=EOF) //輸入
    {
        if(op[0]=='q')  //既然要輸出0~i的所有反向質數,那麼我們需要將它先在樹狀陣列中找出來,才好輸出總和
        {
            k++;    //因為本人習慣於從1開始,而此題從0開始,只得k++
            int l=1,r=tot,mid;  //既然我們要將這個數在樹狀陣列中找出,那麼我們用二分是個不錯的選擇!
            while(l<=r)
            {
                mid=(l+r)/2;    //用mid變數來搜出k的位置
                ll tmp=getsum1(mid);        //記錄一下mid的位置
                if(tmp==k)  //搜出來了就break
                    break;
                else if(tmp<k)  //如果當前的比k小,說明當前數左邊的數也肯定也比k小,所以往右找
                    l=mid+1;
                else    //否則如果當前的比k大,說明當前數右邊的數也肯定也比k大,所以往左找
                    r=mid-1;
            }
            printf("%lld\n",getsum2(mid));  //最後輸出總和就好啦!
        }
        if(op[0]=='d')  
        {
            add1(m[k/10],-1);   //不存在了就減去1,(就是把它刪了)
            add2(m[k/10],-fac[m[k/10]]);    //將這個數從第一個位置到當前位置有多少個反向質數表為0(也是把它刪了^_^)刪數時因為轉換的是6位數,所以要這裡要/10
        }
    }
	return 0;
}

呼——整篇文章就差不多了,我再把完整程式碼整合一下:

#include <bits/stdc++.h>
#define N 1000001   //巨集定義一個最大值
#define ll long long    //巨集定義ll為long long (懶~)
using namespace std;    //名稱空間
bool is_prime[N];   //is_prime[i]表示的是i是否為質數,true則是,false則不是
int prime[N],tot=0,a[N],fac[N];  //prime存貯質數,tot儲存質數有多少,a[i]記錄了prime[i]的反向質數,fac[i]儲存的是i的質因數的個數
ll c1[N],c2[N]; //這裡要定義兩個樹狀陣列,一個樹狀陣列c1記錄當前i節點之前有多少個數,而另一個c2記錄了從第一個位置到當前位置反向質數的質因數的個數的總和
map<int,int>m;  //為了能夠離散化,我使用了map
void Euler_prime()   //尤拉篩
{

	memset(is_prime,true,sizeof(is_prime));     
	is_prime[1]=false;
	for(int i=2;i<N;i++)
	{
		if(is_prime[i]) prime[++tot]=i;
		for(int j=1;j<=tot;j++)
		{
			if(prime[j]*i>N)
				break;
			is_prime[prime[j]*i]=false;
			if(i%prime[j]==0)   
				break;
		}
	}
}
int solve(int num)  //實現了num數的反向
{
    int len=0,ret=0,bit[20];
    while(num)  
    {
        bit[len++]=num%10;
        num/=10;
    }
    for(int i=0;i<len;i++)
    {
        ret=ret*10+bit[i];
    }       //本函式到這裡實現了res=num的反向數
    while(ret<100000) ret*=10;  //因為求出來的數不一定是6位數,所以要加上這行程式碼
    return ret;
}
void init() //初始化函式
{
    Euler_prime();   //先篩了吧……
    for(int i=1;i<=tot;i++)
        a[i]=solve(prime[i]);   //a[i]表示第prime[i]個數的反向質數
    sort(a+1,a+1+tot);  //此題需要排序
    for(int i=1;i<=tot;i++)
        m[a[i]]=i;  //將其離散化
/*以下函式部分實現了求i的質因數*/
    for(int i=1;i<=tot;i++) 
    {
        fac[i]=2;
        int tmp=a[i];
        for(int j=1;j<=tot&&prime[j]*prime[j]<=tmp;j++) 
        {
            while(tmp%prime[j]==0)
            {
                tmp/=prime[j];
                fac[i]++;
            }
        }
        if(tmp>1) fac[i]++; //在特殊情況下,要加一個
    }
}
/**********以下是樹狀陣列基本函式**********/
int lowbit(int k)   //神奇的lowbit函式
{
    return k&-k;
}
void add1(int x,int v)  //
{
    for(int i=x;i<N;i+=lowbit(i))
        c1[i]+=v;
}
void add2(int x,int v)  //
{
    for(int i=x;i<N;i+=lowbit(i))
        c2[i]+=v;
}
ll getsum1(int x)   //
{
    ll ans=0;
    for(int i=x;i>0;i-=lowbit(i))
        ans+=c1[i];
    return ans;
}
ll getsum2(int x)   //
{
    ll ans=0;
    for(int i=x;i>0;i-=lowbit(i))   
        ans+=c2[i];
    return ans;
}
/**********以上是樹狀陣列基本函式**********/
int main()
{
    init(); //初始化......
	char op[2]; //因為一個字元的輸入總是有問題,所以就定義字串啦
    int k;
    for(int i=1;i<=tot;i++)
    {
        add1(i,1);  //存在則壓入數字1
        add2(i,fac[i]); //當前位置有多少個反向質數
    }
    while(scanf("%s%d",op,&k)!=EOF) //輸入
    {
        if(op[0]=='q')  //既然要輸出0~i的所有反向質數,那麼我們需要將它先在樹狀陣列中找出來,才好輸出總和
        {
            k++;    //因為本人習慣於從1開始,而此題從0開始,只得k++
            int l=1,r=tot,mid;  //既然我們要將這個數在樹狀陣列中找出,那麼我們用二分是個不錯的選擇!
            while(l<=r)
            {
                mid=(l+r)/2;    //用mid變數來搜出k的位置
                ll tmp=getsum1(mid);        //記錄一下mid的位置
                if(tmp==k)  //搜出來了就break
                    break;
                else if(tmp<k)  //如果當前的比k小,說明當前數左邊的數也肯定也比k小,所以往右找
                    l=mid+1;
                else    //否則如果當前的比k大,說明當前數右邊的數也肯定也比k大,所以往左找
                    r=mid-1;
            }
            printf("%lld\n",getsum2(mid));  //最後輸出總和就好啦!
        }
        if(op[0]=='d')  
        {
            add1(m[k/10],-1);   //不存在了就減去1,(就是把它刪了)
            add2(m[k/10],-fac[m[k/10]]);    //將這個數從第一個位置到當前位置有多少個反向質數表為0(也是把它刪了^_^)刪數時因為轉換的是6位數,所以要這裡要/10
        }
    }
	return 0;
}
/*
    這麼長的程式碼終於打完了,呼——
    來總結一下:
    - 看到質數,數量眾多,首先想質數篩(推薦尤拉篩)
    - 在資料一對一對映時,為了省時間省財富,我推薦STL的map
    - 做題時,要找出樹狀陣列的基本模型,判斷樹狀陣列維護什麼,需要幾個維護
    - 在查詢時,要學會使用二分,事半功倍
    (其實主要還是樹狀陣列,只不過加了一個質數篩,二分和一些操作而已)
*/