A bit of fun: fun with bits

本文介绍了位运算的基础知识,包括按位与、或、非、异或等操作及其在算法优化中的应用。此外,还详细讲解了如何利用位运算进行集合操作、提取二进制位信息、计算二进制位数量等高级技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

     A bit of fun: fun with bits

http://www.cnblogs.com/drizzlecrj/archive/2007/03/13/672576.html
        
                             【原文见:http://www.topcoder.com/tc?module=Static&d1=tutorials&d2=bitManipulation
                                                                                        作者 By bmerry
                                                                                        TopCoder Member
                                                                                         翻译    农夫三拳@seu
                                                                                         drizzlecrj@gmail.com
Introduction
    TopCoder比赛中的大部分优化都是高端优化;也就是说,它们影响的是算法而不是实现。尽管如此,使用位运算一个非常有用和高效的低端优化,或者使用一个整数的位来表示一个集合。使用它不仅可以在速度和空间上上产生一个巨大的提高,还能同时简化代码。

    在进入更多高级技巧的讨论之前,我先对基本概念进行一个简要的分类。

The basics
    位运算的核心是按位操作 &(and),|(or),~(not)和^(xor)。你应该熟悉前三者的bool运算形式(&&,||和!),下面是真值表:

A B !A A && B A || B A ^ B
0 0 1 0 0 0
0 1 1 0 1 1
1 0 0 0 1 1
1 1 0 1 1 0

这些按位运算操作的变形是一样的,除了在解释参数时不使用true或者false而是对参数的每一个位进行运算。因此,如果A是1010,B是1100,那么
A & B = 1000 
A | B = 1110 
A ^ B = 0110 
~A = 11110101 (1的数目由A的类型决定).

另外两个我们需要的操作符是移位操作符 a << b 和a >> b。前者将a中所有的位向左移动b个位置;而后者向右进行移动。对于非负值(这是我们仅需要关注的),新出现的位使用0进行填充。你可以考虑将b左移b位看作乘以2^b,将右移看作整数处以2^b。移位经常被使用来访问一个特定的位,例如,1 << x是一个x位被设置,而其他位被清楚的二进制数字(位通常总是从最靠右边的/最低有效位进行计数,通常用0标注)

一般来说,我们使用一个整数来表示一个拥有32(或者64,使用一个64位的整数)个值的域的集合,用1来代表元素在集合中,而0表示不在其中。下面的操作都相当的直接,这里ALL_BITS是一个与域元素相对应的全1的数字。
集合并
 A | B
集合交
 A & B
集合差
 A & ~B
集合补集
 ALL_BITS ^ A
设置某个位
 A |= 1 << bit
清除某个位
 A &= ~(1 << bit)
测试某个位
 (A & 1 << bit) != 0

Extracting every last bit
    在这一小节中,我们考虑这样一个问题,找出一个数的最高和最低的1位。对于将一个集合分散成单个元素有一些基本的运算。
    
    通过正确的位运算和算术运算,找出最低位可以非常简单。假设我们想要找到x的最低位(非0位)。如果我们从x中减1,那么这个位就清除了,但是x中其他所有的位都还存在。因此, x & ~(x-1)包含了x的最低设置位。尽管如此,我们得到的仅仅是值,而不是这个位的索引。

     如果我们需要得到最高或者最低位的下标,最明显的方法是遍历位(向前或者向后)直到找到被设置的位。一眼撇过,这看起来很慢,因为它根本没用到位压缩。尽管如此,如果N个元素的域的2^N个子集都算起来,那么这个循环平均使用两个迭代,事实上这个是最快的方法。
    386当中引入了用于位扫描的CPU指令: BSF(bit scan forward)和BSR(bit scan reverse)。GCC使用内建的__builtin_ctz (计数末尾的0)

和__builtin_clz (计数前导0)函数来完成这些操作。对于TopCoder中的C++程序员来说,有最方便的找到位索引的方法。警告:参数为0时返回值未定义。

    最后,有一个可移植的在循环解决方案需要很多迭代的情况下运行很好的方法。使用每种类型的4个或者8个字节的整数来索引一张预先定义好的存储每一个字节里面最高(低)设置位的索引的256个条目的表。整数的最高(低)位就是表中条目的最大(最小)条目。这个方法仅仅是为了完整性而提及,在TopCoder比赛中它获得的效率不太可能得到证明。

Counting out the bits
    可以很容易的判断一个数是不是2的幂次:清除最低的1位(见上面)并且检查结果是不是0.尽管如此,有的时候需要直到有多少个被设置了,这就相对有点难度了。

    GCC有一个叫做__builtin_popcount的内建函数,它可以精确的计算1的个数。尽管如此,不同于__builtin_ctz,它并没有被翻译成一个硬件指令(至少在x86上不是)。相反的,它使用一张类似上面提到的基于表的方法来进行位搜索。这无疑很高效并且非常方便。

    其他语言的使用者没有这个选项(尽管他们可以重新实现这个算法)。如果一个数只有很少的1的位,另外一个方法是重复的获取最低的1位,并且清除它。

All the subsets
    位运算的一个大的优点在于它可以很平常的遍历一个N个元素集合的子集:每一个N位的值表示一些子集。更好一点是,如果A是B的子集,

那么表示A的数要比表示B的小,这对于一些动态规划的解决方案很方便。
    
    它同样可以遍历一个特殊集合的子集(通过一些位串表示),例如让你以倒序进行访问(如果这个是一个问题的话,可以将它们生成的时

候放到列表中,然后从后向前访问)。这个技巧和找一个数字的最低位是类似的。如果我们从子集中减1,那么最低的集合元素被清除了,并且

每一个更小的元素被设置了。尽管如此,我们只想在超集中设置这些较小的元素。因此迭代步骤就是i = (i - 1) & superset。

Even a bit wrong scores zero
    在进行位操作的时候容易犯一些错误。在你的代码中注意以下方面:
 1. 当执行移位操作,例如a << b, x86架构仅使用b的底部5个位(对64位元素是6)。这就意味着左移(右)32位等于什么都没做,

而不是清除所有的位。这个行为在Java和C#语言当中也被指定了;C99中说到移位至少值的大小将得到未定义的结果。历史上:8086使用完全的

移位寄存器,行为上的改变通常用来检测新的处理器。

 2. &和|的优先级比比较运算符要低。这就意味着x & 3 == 1被解释成了x & (3 == 1),这不是我们想要的。

 3. 如果你想书写可移植的C/C++代码,确保使用无符号的类型,尤其当你想要使用最高位的时候。C99中谈到对一个负值进行移位操作

是未定义的。Java仅有有符号类型: >>将使用扩展类型值(这可能并不是我们想要的),但是Java指定的>>>操作符将使用0进行填充。

Cute tricks
     还有一些可以使用微操作进行的技巧。它们可以用来让你的朋友感到惊讶,但是一般来说在实际中并不是值得使用。

     将一个整数的位进行逆转

=  ((x  &   0xaaaaaaaa >>   1 |  ((x  &   0x55555555 <<   1 );
=  ((x  &   0xcccccccc >>   2 |  ((x  &   0x33333333 <<   2 );
=  ((x  &   0xf0f0f0f0 >>   4 |  ((x  &   0x0f0f0f0f <<   4 );
=  ((x  &   0xff00ff00 >>   8 |  ((x  &   0x00ff00ff <<   8 );
=  ((x  &   0xffff0000 >>   16 |  ((x  &   0x0000ffff <<   16 );

作为一个练习,看看你能否将它应用在取得一个字长里面所有的位的数量,遍历{0,1,...N-1}所有k个元素的子集

int  s  =  ( 1   <<  k)  -   1 ;
while  ( ! (s  &   1   <<  N))
{
    
// do stuff with s
    int lo = s & ~(s - 1);       // lowest one bit
    int lz = (s + lo) & ~s;      // lowest zero bit above lo
    s |= lz;                     // add lz to the set
    s &= ~(lz - 1);              // reset bits below lz
    s |= (lz / lo / 2- 1;      // put back right number of bits at end
}

    在C中,最后移行可以写作 s |= (lz >> ffs(lo)) - 1来避免除法。
评估x ? y : -y,这里x是0或者1

( - x ^ y)  +  x

这个可以在二次补码记法的架构上工作(现在你能找到的机器几乎都是这样),这里负数通过将所有变反再加1得到。注意在i686和上面所说的,原始表达式是由于CMOVE(conditional move)指令而变得高效(也就是,没有分支)。

Sample problems
TCCC 2006, Round 1B Medium

    对每一个城市,为它相邻的城市保持一个位集。一旦工厂的部分建筑被选中(递归的),将这些位加在一起将给出一个描述所有部分工厂集合的可能位置。如果这个位集合中拥有k个位,那么将有C(m,k)种方法分配集装工厂。

TCO 2006, Round 1 Easy
    节点的数量暗示可以考虑所有可能的子集。对每一个可能的子集,我们考虑两种可能性:要么最小标号的节点根本不进行通讯,在这种情况下我们在原来的集合中排除它,要么它与一些节点交流,在这种情况下,我们将所有的这些节点排除。结果得到的代码相当短:

static   int  dp[ 1   <<   18 ];

int  SeparateConnections::howMany(vector  < string >  mat)
{
    
int N = mat.size();
    
int N2 = 1 << N;
    dp[
0= 0;
    
for (int i = 1; i < N2; i++)
    
{
        
int bot = i & ~(i - 1);
        
int use = __builtin_ctz(bot);
        dp[i] 
= dp[i ^ bot];
        
for (int j = use + 1; j < N; j++)
            
if ((i & (1 << j)) && mat[use][j] == 'Y')
                dp[i] 
= max(dp[i], dp[i ^ bot ^ (1 << j)] + 2);
    }

    
return dp[N2 - 1];
}


SRM 308, Division 1 Medium
    棋盘上有36个正方形,并且都是不可辨别的,因此所有可能的位置可以编码成一个64位的整数。第一步是枚举所有合法的
移动,任何合法的移动可以使用3个位域表示:一个before状态,一个after状态和一个mask。mask定义了before状态的哪一个部分比较重要。

移动可以从当前的状态得到  if (current & mask) == before;如果得到了当前状态,那么新的状态是(current & ~mask) | after

SRM 320, Division 1 Hard
    条件告诉我们最多8个列(如果有更多的话,我们可以交换行和列),因此考虑每种方法来放置行是可行的。一旦我们有了这些信息,我们

可以解决这个问题(参考match editorial详情)。我们因此需要一个n位的不具有两个相邻1的整数的列表,并且我们也需要知道在每一行当中

有多少个1位。我的代码如下:

for  ( int  i  =   0 ; i  <  ( 1   <<  n); i ++ )
{
    
if (i & (i << 1)) continue;
    pg.push_back(i);
    pgb.push_back(__builtin_popcount(i));
}
//########################################################################### // // FILE: F2806x_PieVect.h // // TITLE: F2806x Devices PIE Vector Table Definitions. // //########################################################################### // $TI Release: F2806x C/C++ Header Files and Peripheral Examples V136 $ // $Release Date: Apr 15, 2013 $ //########################################################################### #ifndef F2806x_PIE_VECT_H #define F2806x_PIE_VECT_H #ifdef __cplusplus extern "C" { #endif //--------------------------------------------------------------------------- // PIE Interrupt Vector Table Definition: // // Create a user type called PINT (pointer to interrupt): typedef interrupt void(*PINT)(void); // Define Vector Table: struct PIE_VECT_TABLE { // Reset is never fetched from this table. // It will always be fetched from 0x3FFFC0 in // boot ROM PINT PIE1_RESERVED; PINT PIE2_RESERVED; PINT PIE3_RESERVED; PINT PIE4_RESERVED; PINT PIE5_RESERVED; PINT PIE6_RESERVED; PINT PIE7_RESERVED; PINT PIE8_RESERVED; PINT PIE9_RESERVED; PINT PIE10_RESERVED; PINT PIE11_RESERVED; PINT PIE12_RESERVED; PINT PIE13_RESERVED; // Non-Peripheral Interrupts: PINT TINT1; // CPU-Timer1 PINT TINT2; // CPU-Timer2 PINT DATALOG; // Datalogging interrupt PINT RTOSINT; // RTOS interrupt PINT EMUINT; // Emulation interrupt PINT NMI; // Non-maskable interrupt PINT ILLEGAL; // Illegal operation TRAP PINT USER1; // User Defined trap 1 PINT USER2; // User Defined trap 2 PINT USER3; // User Defined trap 3 PINT USER4; // User Defined trap 4 PINT USER5; // User Defined trap 5 PINT USER6; // User Defined trap 6 PINT USER7; // User Defined trap 7 PINT USER8; // User Defined trap 8 PINT USER9; // User Defined trap 9 PINT USER10; // User Defined trap 10 PINT USER11; // User Defined trap 11 PINT USER12; // User Defined trap 12 // Group 1 PIE Peripheral Vectors: PINT ADCINT1; // ADC - if Group 10 ADCINT1 is enabled, this must be rsvd1_1 PINT ADCINT2; // ADC - if Group 10 ADCINT2 is enabled, this must be rsvd1_2 PINT rsvd1_3; PINT XINT1; // External Interrupt 1 PINT XINT2; // External Interrupt 2 PINT ADCINT9; // ADC 9 PINT TINT0; // Timer 0 PINT WAKEINT; // WD // Group 2 PIE Peripheral Vectors: PINT EPWM1_TZINT; // EPWM-1 PINT EPWM2_TZINT; // EPWM-2 PINT EPWM3_TZINT; // EPWM-3 PINT EPWM4_TZINT; // EPWM-4 PINT EPWM5_TZINT; // EPWM-5 PINT EPWM6_TZINT; // EPWM-6 PINT EPWM7_TZINT; // EPWM-7 PINT EPWM8_TZINT; // EPWM-8 // Group 3 PIE Peripheral Vectors: PINT EPWM1_INT; // EPWM-1 PINT EPWM2_INT; // EPWM-2 PINT EPWM3_INT; // EPWM-3 PINT EPWM4_INT; // EPWM-4 PINT EPWM5_INT; // EPWM-5 PINT EPWM6_INT; // EPWM-6 PINT EPWM7_INT; // EPWM-7 PINT EPWM8_INT; // EPWM-8 // Group 4 PIE Peripheral Vectors: PINT ECAP1_INT; // ECAP-1 PINT ECAP2_INT; // ECAP-2 PINT ECAP3_INT; // ECAP-3 PINT rsvd4_4; PINT rsvd4_5; PINT rsvd4_6; PINT HRCAP1_INT; // HRCAP-1 PINT HRCAP2_INT; // HRCAP-2 // Group 5 PIE Peripheral Vectors: PINT EQEP1_INT; // EQEP-1 PINT EQEP2_INT; // EQEP-2 PINT rsvd5_3; PINT HRCAP3_INT; // HRCAP-3 PINT HRCAP4_INT; // HRCAP-4 PINT rsvd5_6; PINT rsvd5_7; PINT USB0_INT; // USB-0 // Group 6 PIE Peripheral Vectors: PINT SPIRXINTA; // SPI-A PINT SPITXINTA; // SPI-A PINT SPIRXINTB; // SPI-B PINT SPITXINTB; // SPI-B PINT MRINTA; // McBSP-A PINT MXINTA; // McBSP-A PINT rsvd6_7; PINT rsvd6_8; // Group 7 PIE Peripheral Vectors: PINT DINTCH1; // DMA CH1 PINT DINTCH2; // DMA CH2 PINT DINTCH3; // DMA CH3 PINT DINTCH4; // DMA CH4 PINT DINTCH5; // DMA CH5 PINT DINTCH6; // DMA CH6 PINT rsvd7_7; PINT rsvd7_8; // Group 8 PIE Peripheral Vectors: PINT I2CINT1A; // I2C-A PINT I2CINT2A; // I2C-A PINT rsvd8_3; PINT rsvd8_4; PINT rsvd8_5; PINT rsvd8_6; PINT rsvd8_7; PINT rsvd8_8; // Group 9 PIE Peripheral Vectors: PINT SCIRXINTA; // SCI-A PINT SCITXINTA; // SCI-A PINT SCIRXINTB; // SCI-B PINT SCITXINTB; // SCI-B PINT ECAN0INTA; // eCAN-A PINT ECAN1INTA; // eCAN-A PINT rsvd9_7; PINT rsvd9_8; // Group 10 PIE Peripheral Vectors: PINT rsvd10_1; // Can be ADCINT1, but must make ADCINT1 in Group 1 space "reserved". PINT rsvd10_2; // Can be ADCINT2, but must make ADCINT2 in Group 1 space "reserved". PINT ADCINT3; // ADC PINT ADCINT4; // ADC PINT ADCINT5; // ADC PINT ADCINT6; // ADC PINT ADCINT7; // ADC PINT ADCINT8; // ADC // Group 11 PIE Peripheral Vectors: PINT CLA1_INT1; // CLA PINT CLA1_INT2; // CLA PINT CLA1_INT3; // CLA PINT CLA1_INT4; // CLA PINT CLA1_INT5; // CLA PINT CLA1_INT6; // CLA PINT CLA1_INT7; // CLA PINT CLA1_INT8; // CLA // Group 12 PIE Peripheral Vectors: PINT XINT3; PINT rsvd12_2; PINT rsvd12_3; PINT rsvd12_4; PINT rsvd12_5; PINT rsvd12_6; PINT LVF; // Latched overflow PINT LUF; // Latched underflow }; //--------------------------------------------------------------------------- // PIE Interrupt Vector Table External References & Function Declarations: // extern struct PIE_VECT_TABLE PieVectTable; #ifdef __cplusplus } #endif /* extern "C" */ #endif // end of F2806x_PIE_VECT_H definition //=========================================================================== // End of file. //=========================================================================== /*===================================================================================== File name: sysInit.c Originator: Motor Control Systems Group Description: ===================================================================================== History: ------------------------------------------------------------------------------------- 04-15-2005 Version 3.20 -------------------------------------------------------------------------------------*/ #ifndef _SYSINIT_C #define _SYSINIT_C #include "pmsmahf.h" // These are defined by the linker (see F2812.cmd) extern Uint16 RamfuncsLoadStart; extern Uint16 RamfuncsLoadEnd; extern Uint16 RamfuncsRunStart; /*=====================================================================================*/ /**/ /*=====================================================================================*/ void fun_sysInit() { // ****************************************** // Initialization code for DSP_TARGET = F2812 // ****************************************** // Initialize System Control registers, PLL, WatchDog, Clocks to default state: // This function is found in the DSP281x_SysCtrl.c file. InitSysCtrl(); /* // Globally synchronize all ePWM modules to the time base clock (TBCLK) EALLOW; SysCtrlRegs.PCLKCR0.bit.TBCLKSYNC = 1; EDIS; // HISPCP prescale register settings, normally it will be set to default values EALLOW; // This is needed to write to EALLOW protected registers SysCtrlRegs.HISPCP.all = 0x0000; // SYSCLKOUT/1 EDIS; // This is needed to disable write to EALLOW protected registers */ // Disable and clear all CPU interrupts: DINT; IER = 0x0000; IFR = 0x0000; // Initialize Pie Control Registers To Default State: // This function is found in the DSP281x_PieCtrl.c file. InitPieCtrl(); // Initialize the PIE Vector Table To a Known State: // This function is found in DSP281x_PieVect.c. // This function populates the PIE vector table with pointers // to the shell ISR functions found in DSP281x_DefaultIsr.c. InitPieVectTable(); // Copy time critical code and Flash setup code to RAM // This includes the following ISR functions: EvaTimer1(), EvaTimer2() // EvbTimer3 and and InitFlash(); // The RamfuncsLoadStart, RamfuncsLoadEnd, and RamfuncsRunStart // symbols are created by the linker. Refer to the F2812.cmd file. MemCopy(&RamfuncsLoadStart, &RamfuncsLoadEnd, &RamfuncsRunStart); // Call Flash Initialization to setup flash waitstates // This function must reside in RAM InitFlash(); // User specific functions, Reassign vectors (optional), Enable Interrupts: // Waiting for enable flag set // Enable CNT_zero interrupt using EPWM1 Time-base EALLOW; // This is needed to write to EALLOW protected registers EPwm1Regs.ETSEL.bit.INTEN = 1; // Enable EPWM1INT generation EPwm1Regs.ETSEL.bit.INTSEL = 1; // Enable interrupt CNT_zero event EPwm1Regs.ETPS.bit.INTPRD = 1; // Generate interrupt on the 1st event EPwm1Regs.ETCLR.bit.INT = 1; // Enable more interrupts #if HD_PHASE_OC_SOURCE==HD_PHASE_OC_HARDWARE EPwm1Regs.TZEINT.bit.OST = 1; // Enable EPWM1_TZINT generation #else EPwm1Regs.TZEINT.bit.OST = 0; // Disable EPWM1_TZINT generation #endif EPwm1Regs.TZCLR.bit.OST = 1; // Enable more interrupts EPwm1Regs.TZCLR.bit.CBC = 1; // Enable more interrupts EPwm1Regs.TZCLR.bit.INT = 1; // Enable more interrupts EDIS; // This is needed to disable write to EALLOW protected registers // Reassign ISRs. // Reassign the PIE vector for T1UFINT and CAP3INT to point to a different // ISR then the shell routine found in DSP281x_DefaultIsr.c. // This is done if the user does not want to use the shell ISR routine // but instead wants to use their own ISR. EALLOW; // This is needed to write to EALLOW protected registers PieVectTable.EPWM1_INT = &MainISR; #if HD_PHASE_OC_SOURCE==HD_PHASE_OC_HARDWARE PieVectTable.EPWM1_TZINT = &PdpintISR; #endif EDIS; // This is needed to disable write to EALLOW protected registers // Enable PIE group 3 interrupt 1 for EPWM1_INT PieCtrlRegs.PIEIER3.all = M_INT1; // Enable PIE group 2 interrupt 1 for EPWM1_TZINT PieCtrlRegs.PIEIER2.all = M_INT1; // Enable CPU INT3 for EPWM1_INT: IER |= (M_INT2 | M_INT3); // Initialize scia: /*#if PRODUCT_TYPE_ID==PRODUCT_TYPE_DRIVER_HZ gpioReg.init_HC(&gpioReg); #else*/ gpioReg.init(&gpioReg); //#endif sciCom1.LspClk = ((Uint32)SYSTEM_FREQUENCY*1000000/4); sciCom1.ComNo = SCI_COM1;//usb sciCom1.BaudRate = 9600; sciCom1.init(&sciCom1); // sciCom2.LspClk = ((Uint32)SYSTEM_FREQUENCY*1000000/4); // sciCom2.ComNo = SCI_COM2;//485 // sciCom2.BaudRate = 19200; // sciCom2.init(&sciCom2); fun_errorInit(); fun_ioSignalInit(); fun_dbCtrlInit(); fun_adInit(); fun_eepromInit(); fun_saveRomInit(); ////////////////// fun_machineLockInit(); fun_intfDataInit(); fun_internParaInit(); fun_timerInit(); // fun_masterEncInit(); fun_fanCtrl_init(); fun_sysCtrlInit(); fun_motorDriveInit(); fun_MODBUS_Init(&sciCom1); fun_overLoadInit(); fun_mtAngleOriginInit(); fun_mtoffsetTestInit(); // fun_mtoffsetTestHallInit(); fun_mtAngleInit(); fun_mtCtrlModInit(); fun_dataLogInit(); fun_trqSeltInit(); fun_spdSeltInit(); fun_posSeltInit(); fun_udcSeltInit(); fun_posGenInit(); fun_speedInit(); // fun_appCtrlEMPInit(); fun_mtCtrl_fsm_Init(); fun_appCtrlInit(); fun_runLedInit(); // fun_mtDriveOnInit(); fun_chargCtrlInit(); // fun_appCompInit(); // fun_appDensityInit(); fun_tmpMd_Init(); // fun_appTsn_init(); fun_ad_offsetCheck(); // encoder_para_init(); fun_appCtrl_fsm_Init(); fun_appCtrl_fsm_para_Init(); DI_init(); DI_para_init(); DO_led_init(); DO_init(); encoder_para_init(); //必须在fun_mtEnc_init()之后 error_init_withoutEeprom(); error_init_withEeprom(); production_init(); weftSens_init(); weftSens_para_init(); fun_phaseLackInit(); // fun_testParInit(); fun_fuzzyPIDInit(); // EvaRegs.T1CON.bit.TENABLE = 1; // Enable global Interrupts and higher priority real-time debug events: EnableDog(); EINT; // Enable Global interrupt INTM ERTM; // Enable Global realtime interrupt DBGM } #endif 为什么会触发PdpintISR()这个中断函数
07-19
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值